├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Images ├── Dark.png └── Light.png ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── MonthCal │ ├── Model │ ├── Colors.swift │ ├── Day.swift │ └── Month.swift │ ├── Utility │ ├── GridStack.swift │ └── Helper.swift │ └── ViewModel │ ├── CalendarView.swift │ ├── DayCellView.swift │ ├── File.swift │ ├── MonthView.swift │ └── WeekdaysView.swift └── Tests ├── LinuxMain.swift └── MonthCalTests ├── MonthCalTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Images/Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akeslo/MonthCal/f93ad08d62f4333ee6894a52218b98269bc2070c/Images/Dark.png -------------------------------------------------------------------------------- /Images/Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akeslo/MonthCal/f93ad08d62f4333ee6894a52218b98269bc2070c/Images/Light.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 akeslo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MonthCal", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "MonthCal", 12 | targets: ["MonthCal"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "MonthCal", 23 | dependencies: []), 24 | .testTarget( 25 | name: "MonthCalTests", 26 | dependencies: ["MonthCal"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MonthCal 2 | **A SwiftUI Calendar Generator for IOS** 3 | 4 | ## Features: 5 | - Supply starting date 6 | - Select months to display 7 | - Disabled, Enabled, Selected, Today day states 8 | - Color control 9 | - Dark Mode Support 10 | 11 | ## Photos 12 | ![Light](Images/Light.png) 13 | ![Dark](Images/Dark.png) 14 | 15 | ## Installation 16 | - Requires IOS 13.0+ / Xcode 11+ / Swift 5.1+ 17 | 18 | ### Option 1 19 | - Copy `Sources/MonthCal` files into your xcode project 20 | 21 | ### Option 2 22 | - Add via Swift Package Manager 23 | 24 | ### Configure 25 | - To customize the colors used, edit colors listed in the `Colors.swift` file 26 | 27 | ### Usage 28 | - Show calendar with a start date and amount of months to display 29 | ``` 30 | Import MonthCal 31 | CalendarView(start: Date(), monthsToShow: 6) 32 | ``` 33 | 34 | - Disable ability for dates to be selected 35 | ``` 36 | Import MonthCal 37 | CalendarView(start: Date(), monthsToShow: 6, daysSelectable: false) 38 | ``` 39 | 40 | ## License 41 | - MonthCal is available under the MIT license. See the `LICENSE` file for more info. 42 | 43 | ## Shoutouts 44 | - Paul Hudson - [@twostraws](https://twitter.com/twostraws) - https://www.hackingwithswift.com/100/swiftui 45 | - RaffiKian - https://github.com/RaffiKian/RKCalendar - Inspiration 46 | -------------------------------------------------------------------------------- /Sources/MonthCal/Model/Colors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Colors.swift 3 | // CalSwiftUI 4 | // 5 | // Created by Adam Kes on 11/13/19. 6 | // Copyright © 2019 KesDev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | @available(OSX 10.15, *) 13 | @available(iOS 13.0, *) 14 | class Colors: ObservableObject { 15 | 16 | //Foreground 17 | @Published var textColor: Color = Color.primary 18 | @Published var todayColor: Color = Color.blue 19 | @Published var disabledColor: Color = Color.gray 20 | @Published var selectedColor: Color = Color.primary 21 | 22 | //Background 23 | @Published var backgroundColor: Color = Color.clear 24 | @Published var weekdayBackgroundColor: Color = Color.clear 25 | @Published var selectedBackgroundColor: Color = Color.orange 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/MonthCal/Model/Day.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Day.swift 3 | // CalSwiftUI 4 | // 5 | // Created by Adam Kes on 11/12/19. 6 | // Copyright © 2019 KesDev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | @available(OSX 10.15, *) 13 | @available(iOS 13.0, *) 14 | class Day: ObservableObject { 15 | 16 | @Published var isSelected = false 17 | 18 | var selectableDays: Bool 19 | var dayDate: Date 20 | var dayName: String { 21 | dayDate.dateToString(format: "d") 22 | } 23 | var isToday = false 24 | var disabled = false 25 | let colors = Colors() 26 | var textColor: Color { 27 | if isSelected { 28 | return colors.selectedColor 29 | } else if isToday { 30 | return colors.todayColor 31 | } else if disabled { 32 | return colors.disabledColor 33 | } 34 | return colors.textColor 35 | } 36 | var backgroundColor: Color { 37 | if isSelected { 38 | return colors.selectedBackgroundColor 39 | } else { 40 | return colors.backgroundColor 41 | } 42 | } 43 | 44 | init(date: Date, today: Bool = false, disable: Bool = false, selectable: Bool = true) { 45 | dayDate = date 46 | isToday = today 47 | disabled = disable 48 | selectableDays = selectable 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/MonthCal/Model/Month.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Month.swift 3 | // CalSwiftUI 4 | // 5 | // Created by Adam Kes on 11/12/19. 6 | // Copyright © 2019 KesDev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @available(OSX 10.15, *) 12 | @available(iOS 13.0, *) 13 | struct Month { 14 | 15 | private let calendar = Calendar.current 16 | 17 | var startDate: Date 18 | var selectableDays: Bool 19 | var today = Date() 20 | var monthNameYear: String { 21 | self.monthHeader() 22 | } 23 | var monthDays: [Int: [Day]] { 24 | return self.daysArray() 25 | } 26 | var monthRows: Int { 27 | self.rows() 28 | } 29 | 30 | 31 | private func monthHeader() -> String { 32 | let components = calendar.dateComponents([.year, .month], from: startDate) 33 | let currentMonth = calendar.date(from: components)! 34 | return currentMonth.dateToString(format: "LLLL yyyy") 35 | } 36 | 37 | private func dateToString(date: Date, format: String) -> String { 38 | let dateFormat = DateFormatter.init() 39 | dateFormat.dateFormat = format 40 | let dateString = dateFormat.string(from: date) 41 | return dateString 42 | } 43 | 44 | private func firstOfMonth() -> Date { 45 | let components = calendar.dateComponents([.year, .month], from: startDate) 46 | let firstOfMonth = calendar.date(from: components)! 47 | return firstOfMonth 48 | } 49 | 50 | private func lastOfMonth() -> Date { 51 | var components = DateComponents() 52 | components.month = 1 53 | components.day = -1 54 | let lastOfMonth = calendar.date(byAdding: components, to: firstOfMonth())! 55 | return lastOfMonth 56 | } 57 | 58 | private func dateToWeekday(date: Date) -> Int { 59 | let components = calendar.dateComponents([.weekday], from: date) 60 | guard let weekday = components.weekday else { 61 | fatalError("Cannot convert weekday to Int") 62 | } 63 | return weekday 64 | } 65 | 66 | private func rows() -> Int { 67 | let columns = monthDays.count 68 | var rowCount = 1 69 | for col in 1...columns { 70 | if monthDays[col]!.count > rowCount { 71 | rowCount = monthDays[col]!.count 72 | } 73 | } 74 | return rowCount 75 | } 76 | 77 | private func daysArray() -> [Int: [Day]] { 78 | var arrayOfDays = [ 79 | 1: [Day](), 80 | 2: [Day](), 81 | 3: [Day](), 82 | 4: [Day](), 83 | 5: [Day](), 84 | 6: [Day](), 85 | 7: [Day]() 86 | ] 87 | let fom = firstOfMonth() 88 | let lom = lastOfMonth() 89 | var currentDate = fom 90 | 91 | while (fom <= currentDate && currentDate <= lom) { 92 | let weekday = dateToWeekday(date: currentDate) 93 | let disabled = currentDate > today ? true : false 94 | let currentDateInt = Int(currentDate.dateToString(format: "MMdyy"))! 95 | let todayDateInt = Int(today.dateToString(format: "MMdyy"))! 96 | let isToday = currentDateInt == todayDateInt ? true : false 97 | let currentDay = Day(date: currentDate, today: isToday, disable: disabled, selectable: selectableDays) 98 | arrayOfDays[weekday]?.append(currentDay) 99 | 100 | if fom == currentDate { 101 | var startDay = weekday - 1 102 | while startDay > 0 { 103 | arrayOfDays[startDay]?.append(Day(date: Date(timeIntervalSince1970: 0))) 104 | startDay -= 1 105 | } 106 | } 107 | 108 | if lom == currentDate { 109 | var endDay = weekday + 1 110 | while endDay <= 7 { 111 | arrayOfDays[endDay]?.append(Day(date: Date(timeIntervalSince1970: 0))) 112 | endDay += 1 113 | } 114 | } 115 | 116 | 117 | //Increment date 118 | var components = calendar.dateComponents([.day], from: currentDate) 119 | components.day = +1 120 | currentDate = calendar.date(byAdding: components, to: currentDate)! 121 | } 122 | 123 | return arrayOfDays 124 | } 125 | 126 | 127 | } 128 | -------------------------------------------------------------------------------- /Sources/MonthCal/Utility/GridStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridStack.swift 3 | // CalSwiftUI 4 | // 5 | // Created by Adam Kes on 11/11/19. 6 | // Copyright © 2019 KesDev. All rights reserved. 7 | /* Credit: Paul Hudson, https://www.hackingwithswift.com/quick-start/swiftui/how-to-position-views-in-a-grid 8 | */ 9 | import SwiftUI 10 | 11 | @available(OSX 10.15, *) 12 | @available(iOS 13.0, *) 13 | struct GridStack: View { 14 | 15 | let rows: Int 16 | let cols: Int 17 | let content: (Int, Int) -> Content 18 | 19 | var body: some View { 20 | 21 | VStack { 22 | ForEach(0.. Content) { 39 | self.rows = rows 40 | self.cols = columns 41 | self.content = content 42 | } 43 | 44 | } 45 | 46 | @available(OSX 10.15, *) 47 | @available(iOS 13.0, *) 48 | struct GridStack_Previews: PreviewProvider { 49 | 50 | static var previews: some View { 51 | GridStack(rows: 5, columns: 7) { row, col in 52 | Text("1") 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/MonthCal/Utility/Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helper.swift 3 | // CalSwiftUI 4 | // 5 | // Created by Adam Kes on 11/12/19. 6 | // Copyright © 2019 KesDev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Date { 12 | 13 | func dateToString(format: String) -> String { 14 | let dateFormat = DateFormatter.init() 15 | dateFormat.dateFormat = format 16 | let dateString = dateFormat.string(from: self) 17 | return dateString 18 | } 19 | } 20 | 21 | 22 | extension String { 23 | 24 | func stringToDate(format: String) -> Date { 25 | let dateFormat = DateFormatter.init() 26 | dateFormat.dateFormat = format 27 | let date = dateFormat.date(from: self)! 28 | return date 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/MonthCal/ViewModel/CalendarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarView.swift 3 | // CalSwiftUI 4 | // 5 | // Created by Adam Kes on 11/13/19. 6 | // Copyright © 2019 KesDev. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @available(OSX 10.15, *) 12 | @available(iOS 13.0, *) 13 | public struct CalendarView: View { 14 | 15 | let startDate: Date 16 | let monthsToDisplay: Int 17 | var selectableDays = true 18 | 19 | 20 | public init(start: Date, monthsToShow: Int, daysSelectable: Bool = true) { 21 | self.startDate = start 22 | self.monthsToDisplay = monthsToShow 23 | self.selectableDays = daysSelectable 24 | 25 | } 26 | 27 | public var body: some View { 28 | VStack { 29 | WeekdaysView() 30 | ScrollView { 31 | MonthView(month: Month(startDate: startDate, selectableDays: selectableDays)) 32 | if monthsToDisplay > 1 { 33 | ForEach(1.. Date { 42 | var components = DateComponents() 43 | components.month = add 44 | let next = Calendar.current.date(byAdding: components, to: currentMonth)! 45 | return next 46 | } 47 | 48 | 49 | } 50 | 51 | @available(OSX 10.15, *) 52 | @available(iOS 13.0, *) 53 | struct CalendarView_Previews: PreviewProvider { 54 | static var previews: some View { 55 | CalendarView(start: Date(), monthsToShow: 2) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/MonthCal/ViewModel/DayCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellView.swift 3 | // CalSwiftUI 4 | // 5 | // Created by Adam Kes on 11/13/19. 6 | // Copyright © 2019 KesDev. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @available(OSX 10.15, *) 12 | @available(iOS 13.0, *) 13 | struct DayCellView: View { 14 | 15 | @ObservedObject var day: Day 16 | 17 | var body: some View { 18 | Text(day.dayName).frame(width: 32, height: 32) 19 | .foregroundColor(day.textColor) 20 | .background(day.backgroundColor) 21 | .clipShape(RoundedRectangle(cornerRadius: 10)) 22 | .clipped() 23 | .onTapGesture { 24 | if self.day.disabled == false && self.day.selectableDays { 25 | self.day.isSelected.toggle() 26 | } 27 | } 28 | } 29 | } 30 | 31 | @available(OSX 10.15, *) 32 | @available(iOS 13.0, *) 33 | struct CellView_Previews: PreviewProvider { 34 | static var previews: some View { 35 | DayCellView(day: Day(date: Date())) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/MonthCal/ViewModel/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Adam Kes on 11/13/19. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /Sources/MonthCal/ViewModel/MonthView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MonthView.swift 3 | // CalSwiftUI 4 | // 5 | // Created by Adam Kes on 11/12/19. 6 | // Copyright © 2019 KesDev. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @available(OSX 10.15, *) 12 | @available(iOS 13.0, *) 13 | struct MonthView: View { 14 | 15 | 16 | var month: Month 17 | 18 | var body: some View { 19 | VStack { 20 | Text("\(month.monthNameYear)") 21 | GridStack(rows: month.monthRows, columns: month.monthDays.count) { row, col in 22 | if self.month.monthDays[col+1]![row].dayDate == Date(timeIntervalSince1970: 0) { 23 | Text("").frame(width: 32, height: 32) 24 | } else { 25 | DayCellView(day: self.month.monthDays[col+1]![row]) 26 | } 27 | 28 | } 29 | } 30 | .padding(.bottom, 20) 31 | 32 | } 33 | } 34 | @available(OSX 10.15, *) 35 | @available(iOS 13.0, *) 36 | struct MonthView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | MonthView(month: Month(startDate: Date(), selectableDays: true)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/MonthCal/ViewModel/WeekdaysView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Weekdays.swift 3 | // CalSwiftUI 4 | // 5 | // Created by Adam Kes on 11/13/19. 6 | // Copyright © 2019 KesDev. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @available(OSX 10.15, *) 12 | @available(iOS 13.0, *) 13 | struct WeekdaysView: View { 14 | let weekdays = ["Sun", "Mon", "Tue", "Wen", "Thu", "Fri", "Sat"] 15 | let colors = Colors() 16 | 17 | var body: some View { 18 | HStack { 19 | GridStack(rows: 1, columns: 7) { row, col in 20 | Text(self.weekdays[col]) 21 | } 22 | }.padding(.bottom, 20).background(colors.weekdayBackgroundColor) 23 | } 24 | } 25 | 26 | @available(OSX 10.15, *) 27 | @available(iOS 13.0, *) 28 | struct WeekdaysView_Previews: PreviewProvider { 29 | static var previews: some View { 30 | WeekdaysView() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import MonthCalTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += MonthCalTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/MonthCalTests/MonthCalTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MonthCal 3 | 4 | final class MonthCalTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/MonthCalTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(MonthCalTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------