├── .gitignore
├── Images
├── calendar.png
├── calendar_decorations.png
├── calendar_ja.png
├── calendar_range.png
├── calendar_selections.png
└── calendar_serif.png
├── LICENSE
├── Package.swift
├── README.md
└── Sources
└── CalendarView
├── CalendarView.swift
├── PrivacyInfo.xcprivacy
└── UICalendarView+Helper.swift
/.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 |
--------------------------------------------------------------------------------
/Images/calendar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AllanJuenemann/CalendarView/a0f913aff2bf63a808e60c9490fe034043613c5f/Images/calendar.png
--------------------------------------------------------------------------------
/Images/calendar_decorations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AllanJuenemann/CalendarView/a0f913aff2bf63a808e60c9490fe034043613c5f/Images/calendar_decorations.png
--------------------------------------------------------------------------------
/Images/calendar_ja.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AllanJuenemann/CalendarView/a0f913aff2bf63a808e60c9490fe034043613c5f/Images/calendar_ja.png
--------------------------------------------------------------------------------
/Images/calendar_range.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AllanJuenemann/CalendarView/a0f913aff2bf63a808e60c9490fe034043613c5f/Images/calendar_range.png
--------------------------------------------------------------------------------
/Images/calendar_selections.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AllanJuenemann/CalendarView/a0f913aff2bf63a808e60c9490fe034043613c5f/Images/calendar_selections.png
--------------------------------------------------------------------------------
/Images/calendar_serif.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AllanJuenemann/CalendarView/a0f913aff2bf63a808e60c9490fe034043613c5f/Images/calendar_serif.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Allan Juenemann
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.7
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "CalendarView",
7 | platforms: [.iOS(.v16)],
8 | products: [
9 | .library(
10 | name: "CalendarView",
11 | targets: ["CalendarView"]),
12 | ],
13 | targets: [
14 | .target(
15 | name: "CalendarView",
16 | resources: [.process("PrivacyInfo.xcprivacy")]),
17 | ]
18 | )
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CalendarView
2 |
3 | CalendarView makes UIKit's [UICalendarView](https://developer.apple.com/documentation/uikit/uicalendarview) with all its features available to SwiftUI.
4 |
5 | Please note that `UICalendarView` uses [DateComponents](https://developer.apple.com/documentation/foundation/datecomponents) rather than [Date](https://developer.apple.com/documentation/foundation/date). CalendarView uses the same convention for consistency but might add support for `Date` in the future.
6 |
7 | ## Usage
8 |
9 | ### Displaying the calendar
10 |
11 | ```swift
12 | import SwiftUI
13 | import CalendarView
14 |
15 | var body: some View {
16 | CalendarView()
17 | }
18 | ```
19 |
20 |
21 |
22 | ### Configuring the calendar
23 |
24 | CalendarView uses the [calendar](https://developer.apple.com/documentation/swiftui/environmentvalues/calendar), [time zone](https://developer.apple.com/documentation/swiftui/environmentvalues/timezone) and [locale](https://developer.apple.com/documentation/swiftui/environmentvalues/locale) from the environment.
25 |
26 | ```swift
27 | CalendarView()
28 | .environment(\.locale, .init(identifier: "ja"))
29 | ```
30 |
31 |
32 |
33 | The [font design](https://developer.apple.com/documentation/uikit/uifontdescriptor/systemdesign) can be configured by using the `fontDesign` modifier.
34 |
35 | ```swift
36 | CalendarView()
37 | .fontDesign(.serif)
38 | ```
39 |
40 |
41 |
42 | You can also set the available date range.
43 |
44 | ```swift
45 | CalendarView(availableDateRange: specialEvent)
46 | ```
47 |
48 |
49 |
50 | ### Updating visible components
51 |
52 | You can set and update the current components (year, month) that should be visible in the calendar.
53 |
54 | ```swift
55 | VStack {
56 | CalendarView(visibleDateComponents: $visibleComponents)
57 |
58 | Button("Today") {
59 | withAnimation {
60 | visibleComponents = calendar.dateComponents([.year, .month], from: .now)
61 | }
62 | }
63 | }
64 | ```
65 |
66 | ### Using decorations
67 |
68 | Use the `decorating` modifier to decorate specific days.
69 |
70 | ```swift
71 | CalendarView()
72 | .decorating([DateComponents(day: 16)])
73 | ```
74 |
75 | Decorations can also be customized.
76 |
77 | ```swift
78 | CalendarView()
79 | .decorating(specialDates, systemImage: "star.fill", color: .yellow)
80 | .decorating(otherDates, color: .green, size: .large)
81 | ```
82 |
83 |
84 |
85 | ### Handling selections
86 |
87 | CalendarView supports selections of single and multiple dates.
88 |
89 | ```swift
90 | CalendarView(selection: $selectedDates)
91 | ```
92 |
93 |
94 |
95 | You can also configure which dates are selectable and deselectable.
96 |
97 | ```swift
98 | CalendarView(selection: $selectedDates)
99 | .selectable { dateComponents in
100 | dateComponents.day > 15
101 | }
102 | .deselectable { dateComponents in
103 | dateComponents.year == currentYear && dateComponents.month == currentMonth
104 | }
105 | ```
106 |
--------------------------------------------------------------------------------
/Sources/CalendarView/CalendarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CalendarView.swift
3 | //
4 | // Copyright (c) 2022 Allan Juenemann
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in all
14 | // copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | // SOFTWARE.
23 | //
24 |
25 | import SwiftUI
26 |
27 | public struct CalendarView: UIViewRepresentable {
28 | struct Decoration {
29 | var systemImage: String?
30 | var image: String?
31 | var color: Color?
32 | var size: UICalendarView.DecorationSize?
33 | var customView: ((DateComponents) -> AnyView)?
34 | }
35 |
36 | @Environment(\.calendar) private var calendar
37 | @Environment(\.locale) private var locale
38 | @Environment(\.timeZone) private var timeZone
39 |
40 | private let availableDateRange: DateInterval
41 | private let visibleDateComponents: Binding?
42 | private var selection: Binding?
43 | private var selections: Binding<[DateComponents]>?
44 |
45 | private var fontDesign = UIFontDescriptor.SystemDesign.default
46 | private var canSelectDate: ((DateComponents) -> Bool)?
47 | private var selectableChangeValue: (any Equatable)?
48 | private var canDeselectDate: ((DateComponents) -> Bool)?
49 | private var decorations = [DateComponents: Decoration]()
50 |
51 | // MARK: Initializers
52 |
53 | public init(availableDateRange: DateInterval = .init(start: .distantPast, end: .distantFuture)) {
54 | self.availableDateRange = availableDateRange
55 | self.visibleDateComponents = nil
56 | self.selection = nil
57 | }
58 |
59 | public init(availableDateRange: DateInterval = .init(start: .distantPast, end: .distantFuture), visibleDateComponents: Binding) {
60 | self.availableDateRange = availableDateRange
61 | self.visibleDateComponents = visibleDateComponents
62 | self.selection = nil
63 | }
64 |
65 | public init(availableDateRange: DateInterval = .init(start: .distantPast, end: .distantFuture), selection: Binding) {
66 | self.availableDateRange = availableDateRange
67 | self.visibleDateComponents = nil
68 | self.selection = selection
69 | self.selections = nil
70 | }
71 |
72 | public init(availableDateRange: DateInterval = .init(start: .distantPast, end: .distantFuture), visibleDateComponents: Binding, selection: Binding) {
73 | self.availableDateRange = availableDateRange
74 | self.visibleDateComponents = visibleDateComponents
75 | self.selection = selection
76 | self.selections = nil
77 | }
78 |
79 | public init(availableDateRange: DateInterval = .init(start: .distantPast, end: .distantFuture), selection: Binding<[DateComponents]>) {
80 | self.availableDateRange = availableDateRange
81 | self.visibleDateComponents = nil
82 | self.selections = selection
83 | self.selection = nil
84 | }
85 |
86 | public init(availableDateRange: DateInterval = .init(start: .distantPast, end: .distantFuture), visibleDateComponents: Binding, selection: Binding<[DateComponents]>) {
87 | self.availableDateRange = availableDateRange
88 | self.visibleDateComponents = visibleDateComponents
89 | self.selections = selection
90 | self.selection = nil
91 | }
92 |
93 | // MARK: - UIViewRepresentable
94 |
95 | public func makeUIView(context: Context) -> UICalendarView {
96 | let calendarView = UICalendarView()
97 | calendarView.delegate = context.coordinator
98 |
99 | // must use low compression resistance for horizontal padding and frame modifiers to work properly
100 | calendarView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
101 |
102 | return calendarView
103 | }
104 |
105 | public func updateUIView(_ calendarView: UICalendarView, context: Context) {
106 | context.coordinator.parent = self
107 |
108 | context.coordinator.isUpdatingView = true
109 | defer { context.coordinator.isUpdatingView = false }
110 |
111 | calendarView.calendar = calendar
112 | calendarView.locale = locale
113 | calendarView.timeZone = timeZone
114 | calendarView.fontDesign = fontDesign
115 |
116 | let canAnimate = context.transaction.animation != nil
117 | let visibleYearMonthAtStartOfUpdate = calendarView.visibleDateComponents.yearMonth
118 |
119 | // available date range
120 |
121 | // Make sure the currently visible date components are within range before updating
122 | // availableDateRange. Otherwise, UICalendarView may throw an exception or behave
123 | // in an unexpected way due to internal inconsistencies.
124 | if let visibleDate = calendar.date(from: calendarView.visibleDateComponents), !availableDateRange.contains(visibleDate) {
125 | let newVisibleDate = visibleDate < availableDateRange.start ? availableDateRange.start : availableDateRange.end
126 | calendarView.visibleDateComponents = calendar.dateComponents([.year, .month], from: newVisibleDate)
127 | }
128 |
129 | calendarView.availableDateRange = availableDateRange
130 |
131 | // decorations
132 |
133 | if calendarView.visibleDateComponents.yearMonth == visibleYearMonthAtStartOfUpdate {
134 | // no need to reload decorations if visible date components already changed
135 | calendarView.reloadDecorationsForVisibleMonth(animated: canAnimate)
136 | }
137 |
138 | // selection
139 |
140 | if let selection {
141 | if let dateSelection = calendarView.selectionBehavior as? UICalendarSelectionSingleDate {
142 | if dateSelection.selectedDate?.yearMonthDay != selection.wrappedValue?.yearMonthDay {
143 | dateSelection.setSelected(selection.wrappedValue, animated: canAnimate || selection.canAnimate)
144 | }
145 |
146 | dateSelection.updateSelectableDates()
147 | } else {
148 | let dateSelection = UICalendarSelectionSingleDate(delegate: context.coordinator)
149 | calendarView.selectionBehavior = dateSelection
150 | dateSelection.setSelected(selection.wrappedValue, animated: canAnimate || selection.canAnimate)
151 | }
152 | } else if let selections {
153 | if let dateSelections = calendarView.selectionBehavior as? UICalendarSelectionMultiDate {
154 | dateSelections.setSelectedDates(selections.wrappedValue, animated: canAnimate || selections.canAnimate)
155 | dateSelections.updateSelectableDates()
156 | } else {
157 | let dateSelections = UICalendarSelectionMultiDate(delegate: context.coordinator)
158 | calendarView.selectionBehavior = dateSelections
159 | dateSelections.setSelectedDates(selections.wrappedValue, animated: canAnimate || selections.canAnimate)
160 | }
161 | } else {
162 | // setting selectionBehavior reloads the view which can interfere
163 | // with animations and scrolling, so only set if actually changed
164 | if calendarView.selectionBehavior != nil {
165 | calendarView.selectionBehavior = nil
166 | }
167 | }
168 |
169 | // visible date components
170 |
171 | // Selecting single dates and setting visible date components both potentially scroll the calendar.
172 | // But the visible date components should have the last say because it reflects what is actually shown.
173 |
174 | if let binding = visibleDateComponents {
175 | if let visibleDate = calendar.date(from: binding.wrappedValue) {
176 | // UICalendarView.setVisibleDateComponents throws an exception
177 | // if the new value is not within availableDateRange
178 | if availableDateRange.contains(visibleDate) {
179 | if binding.wrappedValue.yearMonth != calendarView.visibleDateComponents.yearMonth {
180 | calendarView.setVisibleDateComponents(binding.wrappedValue, animated: canAnimate || binding.canAnimate)
181 | }
182 | } else {
183 | let newVisibleDate = visibleDate < availableDateRange.start ? availableDateRange.start : availableDateRange.end
184 | calendarView.visibleDateComponents = calendar.dateComponents([.year, .month], from: newVisibleDate)
185 |
186 | DispatchQueue.main.async {
187 | binding.wrappedValue = calendarView.visibleDateComponents
188 | }
189 | }
190 | }
191 | }
192 | }
193 |
194 | public class Coordinator: NSObject {
195 | var parent: CalendarView
196 | var isUpdatingView = false
197 |
198 | init(_ parent: CalendarView) {
199 | self.parent = parent
200 | }
201 | }
202 |
203 | public func makeCoordinator() -> Coordinator {
204 | Coordinator(self)
205 | }
206 | }
207 |
208 | // MARK: - Font Design
209 |
210 | public extension CalendarView {
211 | func fontDesign(_ design: Font.Design) -> Self {
212 | var view = self
213 |
214 | switch design {
215 | case .default:
216 | view.fontDesign = .default
217 |
218 | case .serif:
219 | view.fontDesign = .serif
220 |
221 | case .rounded:
222 | view.fontDesign = .rounded
223 |
224 | case .monospaced:
225 | view.fontDesign = .monospaced
226 |
227 | @unknown default:
228 | view.fontDesign = .default
229 | }
230 |
231 | return view
232 | }
233 | }
234 |
235 | // MARK: - Decorations
236 |
237 | public extension CalendarView {
238 | func decorating(_ dateComponents: Set, systemImage: String? = nil, color: Color? = nil, size: UICalendarView.DecorationSize = .medium) -> Self {
239 | var view = self
240 | view.add(Decoration(systemImage: systemImage, color: color, size: size), for: dateComponents)
241 | return view
242 | }
243 |
244 | func decorating(_ dateComponents: Set, image: String, color: Color? = nil, size: UICalendarView.DecorationSize = .medium) -> Self {
245 | var view = self
246 | view.add(Decoration(image: image, color: color, size: size), for: dateComponents)
247 | return view
248 | }
249 |
250 | func decorating(_ dateComponents: Set, customView: @escaping () -> some View) -> Self {
251 | var view = self
252 | view.add(Decoration(customView: { _ in AnyView(customView()) }), for: dateComponents)
253 | return view
254 | }
255 |
256 | func decorating(_ dateComponents: Set, customView: @escaping (DateComponents) -> some View) -> Self {
257 | var view = self
258 | view.add(Decoration(customView: { AnyView(customView($0)) }), for: dateComponents)
259 | return view
260 | }
261 |
262 | private mutating func add(_ decoration: Decoration, for dateComponents: Set) {
263 | for key in dateComponents.compactMap(\.decorationKey) {
264 | decorations[key] = decoration
265 | }
266 | }
267 | }
268 |
269 | // MARK: - Selections
270 |
271 | public extension CalendarView {
272 | func selectable(updatingOnChangeOf value: (any Equatable)? = nil, canSelectDate: @escaping (DateComponents) -> Bool) -> Self {
273 | var view = self
274 | view.canSelectDate = canSelectDate
275 | view.selectableChangeValue = value
276 | return view
277 | }
278 |
279 | func deselectable(canDeselectDate: @escaping (DateComponents) -> Bool) -> Self {
280 | var view = self
281 | view.canDeselectDate = canDeselectDate
282 | return view
283 | }
284 |
285 | func deselectable(_ canDeselectDates: Bool = true) -> Self {
286 | deselectable { _ in canDeselectDates }
287 | }
288 | }
289 |
290 | // MARK: - UICalendarViewDelegate
291 |
292 | extension CalendarView.Coordinator: UICalendarViewDelegate {
293 | public func calendarView(_ calendarView: UICalendarView, decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration? {
294 | if #unavailable(iOS 16.2) {
295 | // UICalendarView doesn't provide a way to get notified when property visibleDateComponents changes.
296 | // However, this delegate method is called whenever the user scrolls the view, which in turn
297 | // allows us to read the current value of visibleDateComponents and update the binding.
298 | if !isUpdatingView, let binding = parent.visibleDateComponents {
299 | let visibleComponents = calendarView.visibleDateComponents
300 |
301 | if binding.wrappedValue.yearMonth != visibleComponents.yearMonth {
302 | binding.wrappedValue = visibleComponents
303 | }
304 | }
305 | }
306 |
307 | guard let decoration = parent.decorations.decorationFor(year: dateComponents.year, month: dateComponents.month, day: dateComponents.day) else {
308 | return nil
309 | }
310 |
311 | if let customView = decoration.customView {
312 | return .customView {
313 | let view = UIHostingController(rootView: customView(dateComponents)).view!
314 | view.backgroundColor = .clear
315 | return view
316 | }
317 | }
318 |
319 | let uiColor = decoration.color.flatMap(UIColor.init)
320 | let size = decoration.size ?? .medium
321 |
322 | if let systemImage = decoration.systemImage {
323 | return .image(.init(systemName: systemImage), color: uiColor, size: size)
324 | }
325 |
326 | if let image = decoration.image {
327 | return .image(.init(named: image), color: uiColor, size: size)
328 | }
329 |
330 | return .default(color: uiColor, size: size)
331 | }
332 |
333 | public func calendarView(_ calendarView: UICalendarView, didChangeVisibleDateComponentsFrom previousDateComponents: DateComponents) {
334 | parent.visibleDateComponents?.wrappedValue = calendarView.visibleDateComponents
335 | }
336 | }
337 |
338 | // MARK: - UICalendarSelectionSingleDateDelegate
339 |
340 | extension CalendarView.Coordinator: UICalendarSelectionSingleDateDelegate {
341 | public func dateSelection(_ selection: UICalendarSelectionSingleDate, canSelectDate dateComponents: DateComponents?) -> Bool {
342 | if let dateComponents {
343 | return parent.canSelectDate?(dateComponents) ?? true
344 | }
345 |
346 | if let canDeselectDate = parent.canDeselectDate, let selectedDate = selection.selectedDate {
347 | return canDeselectDate(selectedDate)
348 | }
349 |
350 | return false // UICalendarView's default behavior
351 | }
352 |
353 | public func dateSelection(_ selection: UICalendarSelectionSingleDate, didSelectDate dateComponents: DateComponents?) {
354 | parent.selection?.wrappedValue = dateComponents
355 | }
356 | }
357 |
358 | // MARK: - UICalendarSelectionMultiDateDelegate
359 |
360 | extension CalendarView.Coordinator: UICalendarSelectionMultiDateDelegate {
361 | public func multiDateSelection(_ selection: UICalendarSelectionMultiDate, canSelectDate dateComponents: DateComponents) -> Bool {
362 | parent.canSelectDate?(dateComponents) ?? true
363 | }
364 |
365 | public func multiDateSelection(_ selection: UICalendarSelectionMultiDate, canDeselectDate dateComponents: DateComponents) -> Bool {
366 | parent.canDeselectDate?(dateComponents) ?? true
367 | }
368 |
369 | public func multiDateSelection(_ selection: UICalendarSelectionMultiDate, didSelectDate dateComponents: DateComponents) {
370 | parent.selections?.wrappedValue = selection.selectedDates
371 | }
372 |
373 | public func multiDateSelection(_ selection: UICalendarSelectionMultiDate, didDeselectDate dateComponents: DateComponents) {
374 | // workaround for bug in UICalendarView where deselected date components
375 | // do not match programmatically selected date components
376 | selection.selectedDates.removeAll {
377 | $0.yearMonthDay == dateComponents.yearMonthDay
378 | }
379 |
380 | parent.selections?.wrappedValue = selection.selectedDates
381 | }
382 | }
383 |
384 | // MARK: - Helper
385 |
386 | private extension [DateComponents: CalendarView.Decoration] {
387 | func decorationFor(year: Int?, month: Int?, day: Int?) -> Value? {
388 | self[.init(year: year, month: month, day: day)] ??
389 | self[.init(month: month, day: day)] ??
390 | self[.init(day: day)]
391 | }
392 | }
393 |
394 | private extension DateComponents {
395 | var yearMonth: DateComponents {
396 | DateComponents(year: year, month: month)
397 | }
398 |
399 | var yearMonthDay: DateComponents {
400 | DateComponents(year: year, month: month, day: day)
401 | }
402 |
403 | var decorationKey: DateComponents? {
404 | guard let day else {
405 | return nil
406 | }
407 |
408 | if let year, let month {
409 | return .init(year: year, month: month, day: day)
410 | }
411 |
412 | if let month {
413 | return .init(month: month, day: day)
414 | }
415 |
416 | return .init(day: day)
417 | }
418 | }
419 |
420 | private extension Binding {
421 | var canAnimate: Bool {
422 | transaction.animation != nil
423 | }
424 | }
425 |
426 | // MARK: - Preview
427 |
428 | struct CalendarView_Previews: PreviewProvider {
429 | static var previews: some View {
430 | CalendarView()
431 | }
432 | }
433 |
--------------------------------------------------------------------------------
/Sources/CalendarView/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyTracking
6 |
7 | NSPrivacyAccessedAPITypes
8 |
9 | NSPrivacyCollectedDataTypes
10 |
11 | NSPrivacyTrackingDomains
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Sources/CalendarView/UICalendarView+Helper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Decoration+SwiftUI.swift
3 | //
4 | // Copyright (c) 2024 Allan Juenemann
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in all
14 | // copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | // SOFTWARE.
23 | //
24 |
25 | import UIKit
26 |
27 | extension UICalendarView {
28 | func reloadDecorationsForVisibleMonth(animated: Bool) {
29 | guard let visibleMonth = calendar.date(from: visibleDateComponents) else {
30 | return
31 | }
32 |
33 | var daysToReload = [DateComponents]()
34 |
35 | for day in calendar.range(of: .day, in: .month, for: visibleMonth)! {
36 | var components = visibleDateComponents
37 | components.day = day
38 | daysToReload.append(components)
39 | }
40 |
41 | reloadDecorations(forDateComponents: daysToReload, animated: animated)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------