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