-
Notifications
You must be signed in to change notification settings - Fork 234
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
Conversation
776fb82
to
1a52e56
Compare
@bryn no rush on a review - I'm actually going to revise this a bit to see if I can make this API change purely additive. I think there's a way to manage these IDs internally so that consumers don't need to specify them in their item provider closures. |
@@ -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) |
There was a problem hiding this comment.
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.
/// 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 |
There was a problem hiding this comment.
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.
// MARK: Lifecycle | ||
var body: some View { | ||
content | ||
.id(id) |
There was a problem hiding this comment.
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
Details
This fixes onAppear not getting called when SwiftUI item views were reused. The core issue is that, due to views being reused under the hood, updating the
rootView
of a hosting controller fromReusedHostingController_ofViewType_DayView(3)
toReusedHostingController_ofViewType_DayView(19)
would only trigger a content / data update (the text changing from3
to19
), not an identity update, which is what's really happening here. View reuse is an implementation detail, but it is critical for performance.To demonstrate the issue, I've made a simple SwiftUI day view that reproduces the problem. Here, we update a a local store in
onAppear
, and use that value as a text overlay, making it easy to compare that value vs. the expected value. If there's a reuse issue, the numbers don't match, and we make the overlaid text red.Unfortunately, we need an API change here. The.calendarItemModel
extension onSwiftUI.View
needs to take in anAnyHashable
ID, which we internally use to give the view a stable identifier, even when reused across hosting controllers. For simple cases, one can simply pass aDay
or aMonth
(both of which areHashable
) as the identifier.IDs are set internally, so this is a non-breaking change.
We might also consider providing an API that looks like this in the future:
This closure is a view builder, and leaves the conversion to a
CalendarItemModel<SwiftUIWrapperView<SwiftUI.View>>
as an implementation detail (which also means we would handle applying IDs to views internally). I haven't fully thought through if this is possible, but it'd be a nice improvement.Related Issue
#256
Motivation and Context
Bug fix, make SwiftUI integration better
How Has This Been Tested
Example app
Types of changes
Checklist