diff --git a/CHANGELOG.md b/CHANGELOG.md index 73457245..aad23d62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed Storyboard support by removing the `fatalError` in `init?(coder: NSCoder)` - Fixed an issue that could cause the calendar to layout unnecessarily due to a trait collection change notification - Fixed an issue that could cause off-screen items to appear or disappear instantly, rather than animating in or out during animated content changes +- Fixed an issue (via a breaking API change) that caused a SwiftUI view being used as a calendar item to not receive calls to `onAppear`. Use `calendarItemModel(id:)` to create calendar item models from SwiftUI views. The previous `calendarItemModel` get-only property has been removed. ### Changed - Removed all deprecated code, simplifying the public API in preparation for a 2.0 release diff --git a/Example/HorizonCalendarExample/HorizonCalendarExample/Demo View Controllers/SwiftUIItemModelsDemoViewController.swift b/Example/HorizonCalendarExample/HorizonCalendarExample/Demo View Controllers/SwiftUIItemModelsDemoViewController.swift index 2d561077..97456217 100644 --- a/Example/HorizonCalendarExample/HorizonCalendarExample/Demo View Controllers/SwiftUIItemModelsDemoViewController.swift +++ b/Example/HorizonCalendarExample/HorizonCalendarExample/Demo View Controllers/SwiftUIItemModelsDemoViewController.swift @@ -59,13 +59,13 @@ final class SwiftUIItemModelsDemoViewController: BaseDemoViewController { Spacer() } .padding(.vertical) - .calendarItemModel + .calendarItemModel(id: month) } .dayItemProvider { [calendar, selectedDate] day in let date = calendar.date(from: day.components) let isSelected = date == selectedDate - return SwiftUIDayView(dayNumber: day.day, isSelected: isSelected).calendarItemModel + return SwiftUIDayView(dayNumber: day.day, isSelected: isSelected).calendarItemModel(id: day) } } diff --git a/Example/HorizonCalendarExample/HorizonCalendarExample/Demo View Controllers/SwiftUIScreenDemoViewController.swift b/Example/HorizonCalendarExample/HorizonCalendarExample/Demo View Controllers/SwiftUIScreenDemoViewController.swift index 28229228..31a5fbdf 100644 --- a/Example/HorizonCalendarExample/HorizonCalendarExample/Demo View Controllers/SwiftUIScreenDemoViewController.swift +++ b/Example/HorizonCalendarExample/HorizonCalendarExample/Demo View Controllers/SwiftUIScreenDemoViewController.swift @@ -105,12 +105,12 @@ struct SwiftUIScreenDemo: View { Spacer() } .padding() - .calendarItemModel + .calendarItemModel(id: month) } else { return Text(monthHeaderText) .font(.title2) .padding() - .calendarItemModel + .calendarItemModel(id: month) } } @@ -122,7 +122,7 @@ struct SwiftUIScreenDemo: View { isSelected = false } return SwiftUIDayView(dayNumber: day.day, isSelected: isSelected) - .calendarItemModel + .calendarItemModel(id: day) } .dayRangeItemProvider(for: selectedDateRanges) { dayRangeLayoutContext in diff --git a/README.md b/README.md index fe63e622..9af3ef5b 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,7 @@ Using a SwiftUI view is even easier - simply initialize your SwiftUI view and ca Text("\(day.day)") .font(.system(size: 18)) .foregroundColor(Color(UIColor.darkGray)) - .calendarItemModel + .calendarItemModel(id: day) // Use this to ensure stable and unique view identity } ``` diff --git a/Sources/Public/CalendarItemModel.swift b/Sources/Public/CalendarItemModel.swift index 49899c11..feeb2280 100644 --- a/Sources/Public/CalendarItemModel.swift +++ b/Sources/Public/CalendarItemModel.swift @@ -168,10 +168,15 @@ extension View { /// /// - Warning: Using a SwiftUI view with the calendar will cause `SwiftUIView.HostingController`(s) to be added to the /// closest view controller in the responder chain in relation to the `CalendarView`. - public var calendarItemModel: CalendarItemModel> { - CalendarItemModel>( - invariantViewProperties: .init(initialContent: self), - content: .init(content: self)) + /// + /// - Parameters: + /// - id: An ID that uniquely identifies this item relative to other items in the calendar. For simple cases, this can be a `Day` for + /// day items or a `Month` for month header items, for example. + public func calendarItemModel(id: AnyHashable) -> CalendarItemModel> { + let contentAndID = SwiftUIWrapperView.ContentAndID(content: self, id: UUID()) + return CalendarItemModel>( + invariantViewProperties: .init(initialContentAndID: contentAndID), + content: contentAndID) } } diff --git a/Sources/Public/ItemViews/SwiftUIWrapperView.swift b/Sources/Public/ItemViews/SwiftUIWrapperView.swift index 00b23b07..a246b9cc 100644 --- a/Sources/Public/ItemViews/SwiftUIWrapperView.swift +++ b/Sources/Public/ItemViews/SwiftUIWrapperView.swift @@ -30,8 +30,10 @@ public final class SwiftUIWrapperView: UIView { // MARK: Lifecycle - public init(content: Content) { - self.content = content + public init(contentAndID: ContentAndID) { + self.contentAndID = contentAndID + hostingController = HostingController( + rootView: .init(content: contentAndID.content, id: contentAndID.id)) super.init(frame: .zero) @@ -69,16 +71,16 @@ public final class SwiftUIWrapperView: UIView { // MARK: Fileprivate - fileprivate var content: Content { + fileprivate var contentAndID: ContentAndID { didSet { - hostingController.rootView = content + hostingController.rootView = .init(content: contentAndID.content, id: contentAndID.id) configureGestureRecognizers() } } // MARK: Private - private lazy var hostingController = HostingController(rootView: content) + private let hostingController: HostingController> private weak var hostingControllerView: UIView? @@ -126,8 +128,8 @@ extension SwiftUIWrapperView: CalendarItemViewRepresentable { // MARK: Lifecycle - init(initialContent: Content) { - self.initialContent = initialContent + init(initialContentAndID: ContentAndID) { + self.initialContentAndID = initialContentAndID } // MARK: Public @@ -142,39 +144,44 @@ extension SwiftUIWrapperView: CalendarItemViewRepresentable { // MARK: Fileprivate - fileprivate let initialContent: Content + fileprivate let initialContentAndID: ContentAndID } - public struct ContentWrapper: Equatable { + public struct ContentAndID: Equatable { // MARK: Lifecycle - public init(content: Content) { + public init(content: Content, id: AnyHashable) { self.content = content + self.id = id } // MARK: Public - public static func == (_: ContentWrapper, _: ContentWrapper) -> Bool { + public static func == (_: ContentAndID, _: ContentAndID) -> Bool { false } // MARK: Fileprivate fileprivate let content: Content + fileprivate let id: AnyHashable } public static func makeView( withInvariantViewProperties invariantViewProperties: InvariantViewProperties) - -> SwiftUIWrapperView + -> SwiftUIWrapperView { - SwiftUIWrapperView(content: invariantViewProperties.initialContent) + SwiftUIWrapperView(contentAndID: invariantViewProperties.initialContentAndID) } - public static func setContent(_ content: ContentWrapper, on view: SwiftUIWrapperView) { - view.content = content.content + public static func setContent( + _ contentAndID: ContentAndID, + on view: SwiftUIWrapperView) + { + view.contentAndID = contentAndID } } @@ -188,35 +195,48 @@ extension UIResponder { } } -// MARK: - SwiftUIWrapperView.HostingController +// MARK: - IDWrapperView +/// A wrapper view that uses the `id(_:)` modifier on the wrapped view so that each one has its own identity, even if it was reused. @available(iOS 13.0, *) -extension SwiftUIWrapperView { +private struct IDWrapperView: View { - /// The `UIHostingController` type used by `SwiftUIWrapperView` to embed SwiftUI views in a UIKit view hierarchy. - public final class HostingController: UIHostingController { + let content: Content + let id: AnyHashable - // MARK: Lifecycle + var body: some View { + content + .id(id) + } - fileprivate override init(rootView: Content) { - super.init(rootView: rootView) +} - // This prevents the safe area from affecting layout. - _disableSafeArea = true - } +// MARK: - HostingController - @MainActor required dynamic init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } +/// The `UIHostingController` type used by `SwiftUIWrapperView` to embed SwiftUI views in a UIKit view hierarchy. This +/// exists to disable safe area insets and set the background color to clear. +@available(iOS 13.0, *) +private final class HostingController: UIHostingController { - public override func viewDidLoad() { - super.viewDidLoad() + // MARK: Lifecycle - // Override the default `.systemBackground` color since `CalendarView` subviews should be - // clear. - view.backgroundColor = .clear - } + override init(rootView: Content) { + super.init(rootView: rootView) + + // This prevents the safe area from affecting layout. + _disableSafeArea = true + } + + @MainActor required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + // Override the default `.systemBackground` color since `CalendarView` subviews should be + // clear. + view.backgroundColor = .clear } }