Skip to content

Commit

Permalink
Fix SwiftUI item view onAppear not getting called
Browse files Browse the repository at this point in the history
  • Loading branch information
bryankeller committed Sep 8, 2023
1 parent 1e42185 commit 1a52e56
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 44 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ struct SwiftUIScreenDemo: View {
Spacer()
}
.padding()
.calendarItemModel
.calendarItemModel(id: month)
} else {
return Text(monthHeaderText)
.font(.title2)
.padding()
.calendarItemModel
.calendarItemModel(id: month)
}
}

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```

Expand Down
13 changes: 9 additions & 4 deletions Sources/Public/CalendarItemModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<SwiftUIWrapperView<Self>> {
CalendarItemModel<SwiftUIWrapperView<Self>>(
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<SwiftUIWrapperView<Self>> {
let contentAndID = SwiftUIWrapperView.ContentAndID(content: self, id: id)
return CalendarItemModel<SwiftUIWrapperView<Self>>(
invariantViewProperties: .init(initialContentAndID: contentAndID),
content: contentAndID)
}

}
88 changes: 54 additions & 34 deletions Sources/Public/ItemViews/SwiftUIWrapperView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ public final class SwiftUIWrapperView<Content: View>: 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)

Expand Down Expand Up @@ -69,16 +71,16 @@ public final class SwiftUIWrapperView<Content: View>: 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<Content>(rootView: content)
private let hostingController: HostingController<IDWrapperView<Content>>

private weak var hostingControllerView: UIView?

Expand Down Expand Up @@ -126,8 +128,8 @@ extension SwiftUIWrapperView: CalendarItemViewRepresentable {

// MARK: Lifecycle

init(initialContent: Content) {
self.initialContent = initialContent
init(initialContentAndID: ContentAndID) {
self.initialContentAndID = initialContentAndID
}

// MARK: Public
Expand All @@ -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<Content>
-> SwiftUIWrapperView<Content>
{
SwiftUIWrapperView<Content>(content: invariantViewProperties.initialContent)
SwiftUIWrapperView<Content>(contentAndID: invariantViewProperties.initialContentAndID)
}

public static func setContent(_ content: ContentWrapper, on view: SwiftUIWrapperView<Content>) {
view.content = content.content
public static func setContent(
_ contentAndID: ContentAndID,
on view: SwiftUIWrapperView<Content>)
{
view.contentAndID = contentAndID
}

}
Expand All @@ -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<Content: View>: View {

/// The `UIHostingController` type used by `SwiftUIWrapperView` to embed SwiftUI views in a UIKit view hierarchy.
public final class HostingController<Content: View>: UIHostingController<Content> {
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<Content: View>: UIHostingController<Content> {

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
}

}

0 comments on commit 1a52e56

Please sign in to comment.