├── 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 |
--------------------------------------------------------------------------------