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

Conversation

bryankeller
Copy link
Contributor

@bryankeller bryankeller commented Sep 8, 2023

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 from ReusedHostingController_ofViewType_DayView(3) to ReusedHostingController_ofViewType_DayView(19) would only trigger a content / data update (the text changing from 3 to 19), 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.

Reuse issue After fix
Simulator Screen Recording - iPhone 14 Pro - 2023-09-08 at 11 26 51 Simulator Screen Recording - iPhone 14 Pro - 2023-09-08 at 11 26 12
private final class Store: ObservableObject {
  @Published var value: Int?
}

struct SwiftUIDayView: View {

  let dayNumber: Int

  @StateObject private var store = Store()

  var body: some View {
    ZStack(alignment: .center) {
      Text("\(dayNumber)")

      if let storeValue = store.value {
        Text("\(storeValue)")
          .foregroundColor((storeValue == dayNumber ? Color.green : Color.red).opacity(0.5))
      }
    }
    .onAppear {
      store.value = dayNumber
    }
  }
}

Unfortunately, we need an API change here. The .calendarItemModel extension on SwiftUI.View needs to take in an AnyHashable 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 a Day or a Month (both of which are Hashable) 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:

CalendarViewRepresentable(...)
  .dayView { dayComponents in
    Text(dayComponents.day)
  }

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

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.

@bryankeller bryankeller force-pushed the bk/fix-swiftui-item-onAppear branch from 776fb82 to 1a52e56 Compare September 8, 2023 18:34
@bryankeller
Copy link
Contributor Author

@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)
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.

Comment on lines +225 to +248
/// 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
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.

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

@bryankeller bryankeller added enhancement New feature or request bug Something isn't working labels Sep 11, 2023
brynbodayle
brynbodayle previously approved these changes Sep 13, 2023
@bryankeller bryankeller merged commit 54cd7f5 into master Sep 13, 2023
2 checks passed
@bryankeller bryankeller deleted the bk/fix-swiftui-item-onAppear branch September 13, 2023 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants