Skip to content

Commit

Permalink
Fix SwiftUI item view onAppear not getting called (#265)
Browse files Browse the repository at this point in the history
* Fix SwiftUI item view onAppear not getting called

* Set IDs internally and make this a non-breaking change

* Fix tests
  • Loading branch information
bryankeller authored Sep 13, 2023
1 parent 1e42185 commit 54cd7f5
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 41 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 that caused a SwiftUI view being used as a calendar item to not receive calls to `onAppear`

### 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 @@ -121,8 +121,7 @@ struct SwiftUIScreenDemo: View {
} else {
isSelected = false
}
return SwiftUIDayView(dayNumber: day.day, isSelected: isSelected)
.calendarItemModel
return SwiftUIDayView(dayNumber: day.day, isSelected: isSelected).calendarItemModel
}

.dayRangeItemProvider(for: selectedDateRanges) { dayRangeLayoutContext in
Expand Down
2 changes: 2 additions & 0 deletions Sources/Public/AnyCalendarItemModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public protocol AnyCalendarItemModel {
/// - Note: There is no reason to invoke this function from your feature code; it should only be invoked internally.
func _isContentEqual(toContentOf other: AnyCalendarItemModel) -> Bool

mutating func _setSwiftUIWrapperViewContentIDIfNeeded(_ id: AnyHashable)

}

// MARK: - _CalendarItemViewDifferentiator
Expand Down
32 changes: 28 additions & 4 deletions Sources/Public/CalendarItemModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,23 @@ public struct CalendarItemModel<ViewRepresentable>: AnyCalendarItemModel where
return content == other.content
}

public mutating func _setSwiftUIWrapperViewContentIDIfNeeded(_ id: AnyHashable) {
guard
var content = content as? SwiftUIWrapperViewContentIDUpdatable,
content.id == AnyHashable(PlaceholderID.placeholderID)
else {
return
}
content.id = id
self.content = content as? ViewRepresentable.Content
}

// MARK: Private

private let invariantViewProperties: ViewRepresentable.InvariantViewProperties
private let content: ViewRepresentable.Content?

// This is only mutable because we need to update the ID for `SwiftUIWrapperView`'s content.
private var content: ViewRepresentable.Content?

}

Expand Down Expand Up @@ -169,9 +182,20 @@ 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))
let contentAndID = SwiftUIWrapperView.ContentAndID(
content: self,
id: PlaceholderID.placeholderIDAnyHashable)
return CalendarItemModel<SwiftUIWrapperView<Self>>(
invariantViewProperties: .init(initialContentAndID: contentAndID),
content: contentAndID)
}

}

// MARK: - PlaceholderID

/// This exists only to facilitate internally updating the ID of a `SwiftUIWrapperView`'s content.
private enum PlaceholderID: Hashable {
case placeholderID
static let placeholderIDAnyHashable = AnyHashable(PlaceholderID.placeholderID)
}
4 changes: 3 additions & 1 deletion Sources/Public/CalendarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,9 @@ public final class CalendarView: UIView {
}

private func configureView(_ view: ItemView, with visibleItem: VisibleItem) {
view.calendarItemModel = visibleItem.calendarItemModel
var calendarItemModel = visibleItem.calendarItemModel
calendarItemModel._setSwiftUIWrapperViewContentIDIfNeeded(visibleItem.itemType)
view.calendarItemModel = calendarItemModel
view.itemType = visibleItem.itemType
view.frame = visibleItem.frame.alignedToPixels(forScreenWithScale: scale)

Expand Down
97 changes: 63 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,24 +144,29 @@ extension SwiftUIWrapperView: CalendarItemViewRepresentable {

// MARK: Fileprivate

fileprivate let initialContent: Content
fileprivate let initialContentAndID: ContentAndID

}

public struct ContentWrapper: Equatable {
public struct ContentAndID: Equatable, SwiftUIWrapperViewContentIDUpdatable {

// 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: Internal

var id: AnyHashable

// MARK: Fileprivate

fileprivate let content: Content
Expand All @@ -168,17 +175,26 @@ extension SwiftUIWrapperView: CalendarItemViewRepresentable {

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
}

}

// MARK: - SwiftUIWrapperViewContentIDUpdatable

protocol SwiftUIWrapperViewContentIDUpdatable {
var id: AnyHashable { get set }
}

// MARK: UIResponder Next View Controller Helper

extension UIResponder {
Expand All @@ -188,35 +204,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
}

}
2 changes: 2 additions & 0 deletions Tests/ItemViewReuseManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -521,4 +521,6 @@ private struct MockCalendarItemModel: AnyCalendarItemModel, Equatable {
false
}

mutating func _setSwiftUIWrapperViewContentIDIfNeeded(_ id: AnyHashable) { }

}

0 comments on commit 54cd7f5

Please sign in to comment.