├── Social-Image.png ├── .gitignore ├── Tests └── SelectableCalendarViewTests │ └── SelectableCalendarViewTests.swift ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Sources └── SelectableCalendarView │ ├── Calendar+Extension.swift │ ├── View+Extension.swift │ ├── Date+Extension.swift │ └── SelectableCalendarView.swift ├── LICENSE ├── Package.swift └── README.md /Social-Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mszpro/Selectable-Calendar-View/HEAD/Social-Image.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Tests/SelectableCalendarViewTests/SelectableCalendarViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SelectableCalendarView 3 | 4 | final class SelectableCalendarViewTests: XCTestCase { 5 | func testExample() throws { 6 | XCTAssert(true) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/SelectableCalendarView/Calendar+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Calendar+Extension.swift 3 | // SelectableCalendarView 4 | // 5 | // Created by Shunzhe on 2022/04/19. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(iOS 10.0, *) 11 | @available(macOS 10.12, *) 12 | extension Calendar { 13 | 14 | func generateDates(inside interval: DateInterval, matching components: DateComponents) -> [Date] { 15 | var dates = [interval.start] 16 | enumerateDates(startingAfter: interval.start, 17 | matching: components, 18 | matchingPolicy: .nextTime 19 | ) { date, _, stop in 20 | if let date = date { 21 | if date < interval.end { 22 | dates.append(date) 23 | } else { 24 | stop = true 25 | } 26 | } 27 | } 28 | return dates 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [Shunzhe] (mszpro.com) 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 | -------------------------------------------------------------------------------- /Sources/SelectableCalendarView/View+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Extension.swift 3 | // SelectableCalendarView 4 | // 5 | // Created by Shunzhe on 2022/04/19. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | @available(iOS 15, *) 12 | @available(macOS 12, *) 13 | extension View { 14 | @ViewBuilder 15 | func addCircularBackground(isFilled: Bool, isSelected: Bool, highlightColor: Color = .blue, normalColor: Color = .cyan) -> some View { 16 | self 17 | .padding(9) 18 | #if os(macOS) 19 | .foregroundColor(Color(cgColor: .black)) 20 | #elseif os(iOS) 21 | .foregroundColor(Color(uiColor: .systemBackground)) 22 | #endif 23 | .background( 24 | Circle() 25 | .foregroundColor(isSelected ? highlightColor : normalColor) 26 | .frame(width: 35, height: 35) 27 | .opacity(isFilled ? 1.0 : 0.5) 28 | .padding(isSelected ? 3 : 0) 29 | .overlay( 30 | Circle() 31 | .stroke(highlightColor, lineWidth: isSelected ? 2 : 0) 32 | ) 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 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: "SelectableCalendarView", 8 | platforms: [ 9 | .iOS(.v15), 10 | .macOS(.v12) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "SelectableCalendarView", 16 | targets: ["SelectableCalendarView"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "SelectableCalendarView", 27 | dependencies: []), 28 | .testTarget( 29 | name: "SelectableCalendarViewTests", 30 | dependencies: ["SelectableCalendarView"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Selectable Calendar View 2 | 3 | A SwiftUI calendar view that allows month switching and date picking. 4 | 5 | 6 | 7 | ## Usage 8 | 9 | You can simply add this repository to your project using Swift Package, `import SelectableCalendarView` into your SwiftUI view code, and then use the following code to show a calendar: 10 | 11 | ```swift 12 | import SelectableCalendarView 13 | struct ContentView: View { 14 | 15 | @State private var dateSelected: Date = Date() 16 | 17 | var body: some View { 18 | // monthToDisplay変数には、表示させたい月の任意の日を指定します。 19 | SelectableCalendarView(monthToDisplay: Date(), dateSelected: $dateSelected) 20 | } 21 | 22 | } 23 | ``` 24 | 25 | You can also customize the colors of the calendar view, or to show/hide the month switching buttons by adjusting the .init parameters: 26 | 27 | ```swift 28 | public struct SelectableCalendarView: View { 29 | public init(monthToDisplay: Date, dateSelected: Binding, allowSwitchMonth: Bool = true, showMonthLabel: Bool = true, isDateCircleFilled: ((Date) -> Bool)? = nil) { ... } 30 | } 31 | ``` 32 | 33 | - `monthToDisplay` is any day of the month you want to display. 34 | - `dateSelected` is the way your app reads/sets the date selected on the calendar view. 35 | - `allowSwitchMonth` controls whether the previous/next/current month switching buttons are shown. 36 | - `showMonthLabel` cnotrols whether the name of the month is shown or not. 37 | - `isDateCircleFilled` is a delegate-style function you use to indicate whether a date circle should be highlighted. 38 | 39 | ## Install 40 | 41 | Add a new Swift Package with the repository URL. 42 | 43 | See [here](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app) to learn how to add a package to your project. 44 | 45 | The URL of this repository is: https://github.com/mszpro/Selectable-Calendar-View 46 | -------------------------------------------------------------------------------- /Sources/SelectableCalendarView/Date+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+Extension.swift 3 | // SelectableCalendarView 4 | // 5 | // Created by Shunzhe on 2022/04/19. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(iOS 10.0, *) 11 | @available(macOS 10.12, *) 12 | extension Date { 13 | 14 | func getDayNumber()->Int { 15 | return Calendar.current.component(.day, from: self) 16 | } 17 | 18 | func getMonthString() -> String { 19 | let formatter = DateFormatter() 20 | formatter.dateFormat = "YYYY/MM" 21 | return formatter.string(from: self) 22 | } 23 | 24 | func getDaysForMonth() -> [Date] { 25 | guard 26 | let monthInterval = Calendar.current.dateInterval(of: .month, for: self), 27 | let monthFirstWeek = Calendar.current.dateInterval(of: .weekOfMonth, for: monthInterval.start), 28 | let monthLastWeek = Calendar.current.dateInterval(of: .weekOfMonth, for: monthInterval.end) 29 | else { 30 | return [] 31 | } 32 | let resultDates = Calendar.current.generateDates(inside: DateInterval(start: monthFirstWeek.start, end: monthLastWeek.end), 33 | matching: DateComponents(hour: 0, minute: 0, second: 0)) 34 | return resultDates 35 | } 36 | 37 | func isSameDay(comparingTo: Date) -> Bool { 38 | let selfComponents = Calendar.current.dateComponents([.year, .month, .day], from: self) 39 | let comparingComponents = Calendar.current.dateComponents([.year, .month, .day], from: comparingTo) 40 | guard let selfYear = selfComponents.year, 41 | let selfMonth = selfComponents.month, 42 | let selfDay = selfComponents.day, 43 | let comparingYear = comparingComponents.year, 44 | let comparingMonth = comparingComponents.month, 45 | let comparingDay = comparingComponents.day else { 46 | return false 47 | } 48 | return selfYear == comparingYear && 49 | selfMonth == comparingMonth && 50 | selfDay == comparingDay 51 | } 52 | 53 | func getLastMonth() -> Date { 54 | return Calendar.current.date(byAdding: .month, value: -1, to: self) ?? self 55 | } 56 | 57 | func getNextMonth() -> Date { 58 | return Calendar.current.date(byAdding: .month, value: 1, to: self) ?? self 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Sources/SelectableCalendarView/SelectableCalendarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarView.swift 3 | // SelectableCalendarView 4 | // 5 | // Created by Shunzhe on 2022/04/19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(macOS 12, *) 11 | @available(iOS 15, *) 12 | public struct SelectableCalendarView: View { 13 | 14 | // 表示される月の任意の日に設定します(通常は1日) 15 | @State var monthToDisplay: Date 16 | 17 | // The date user selected 18 | @Binding var dateSelected: Date 19 | 20 | // If user can switch to previous month or next month 21 | var allowSwitchMonth: Bool 22 | 23 | // Whether to show the month label 24 | var showMonthLabel: Bool 25 | 26 | // Provide a function; this class will ask if a specific date circle should be filled in (a darker color). 27 | var isDateCircleFilled: ((Date) -> Bool)? 28 | 29 | public init(monthToDisplay: Date, dateSelected: Binding, allowSwitchMonth: Bool = true, showMonthLabel: Bool = true, isDateCircleFilled: ((Date) -> Bool)? = nil) { 30 | self._monthToDisplay = .init(initialValue: monthToDisplay) 31 | self._dateSelected = dateSelected 32 | self.allowSwitchMonth = allowSwitchMonth 33 | self.showMonthLabel = showMonthLabel 34 | self.isDateCircleFilled = isDateCircleFilled 35 | } 36 | 37 | public var body: some View { 38 | VStack { 39 | HStack { 40 | if allowSwitchMonth { 41 | Image(systemName: "arrow.left.circle") 42 | .font(.system(size: 30)) 43 | .onTapGesture { 44 | monthToDisplay = monthToDisplay.getLastMonth() 45 | } 46 | if monthToDisplay.getMonthString() != Date().getMonthString() { 47 | // Show the user return to current month button 48 | Image(systemName: "arrow.counterclockwise.circle") 49 | .font(.system(size: 30)) 50 | .onTapGesture { 51 | monthToDisplay = Date() 52 | } 53 | } 54 | } 55 | Spacer() 56 | if showMonthLabel { 57 | Text(monthToDisplay.getMonthString()) 58 | .font(.title2) 59 | } 60 | Spacer() 61 | if allowSwitchMonth { 62 | Image(systemName: "arrow.right.circle") 63 | .font(.system(size: 30)) 64 | .onTapGesture { 65 | monthToDisplay = monthToDisplay.getNextMonth() 66 | } 67 | } 68 | } 69 | LazyVGrid(columns: Array(repeating: GridItem(), count: 7)) { 70 | // Week day labels 71 | ForEach(["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"], id: \.self) { weekdayName in 72 | Text(weekdayName) 73 | .font(.system(size: 15).bold()) 74 | } 75 | // Day number text 76 | Section { 77 | ForEach(monthToDisplay.getDaysForMonth(), id: \.self) { date in 78 | // Only display days of the given month 79 | if Calendar.current.isDate(date, equalTo: monthToDisplay, toGranularity: .month) { 80 | if let isDateCircleFilled = isDateCircleFilled { 81 | Text("\(date.getDayNumber())") 82 | .font(.system(size: 15)) 83 | .id(date) 84 | .addCircularBackground(isFilled: isDateCircleFilled(date), isSelected: dateSelected.isSameDay(comparingTo: date)) 85 | .onTapGesture { 86 | self.dateSelected = date 87 | } 88 | } else { 89 | Text("\(date.getDayNumber())") 90 | .font(.system(size: 15)) 91 | .id(date) 92 | .addCircularBackground(isFilled: true, isSelected: dateSelected.isSameDay(comparingTo: date)) 93 | .onTapGesture { 94 | self.dateSelected = date 95 | } 96 | } 97 | } else { 98 | Text("\(date.getDayNumber())") 99 | .font(.system(size: 15)) 100 | .addCircularBackground(isFilled: false, isSelected: false) 101 | .hidden() 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | } 110 | --------------------------------------------------------------------------------