Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix SwiftUI item view onAppear not getting called #265

Merged
merged 3 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Contributor Author

@bryankeller bryankeller Sep 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind of annoying that we need this but we're working with a type-erased AnyCalendarItemModel, so we don't know if the original CalendarItemModel<CalendarItemViewRepresentable> type is a SwiftUIWrapperView or not.


}

// 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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the magic line that adds a stable 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
Comment on lines +225 to +248
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unchanged, but I pulled it out of the SwiftUIWrapperView extension since it was private anyways.

}

}
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) { }

}
Loading