From af28019e625a5549c060db3ac150e8bbf9d27066 Mon Sep 17 00:00:00 2001 From: Bryan Keller Date: Mon, 22 Apr 2024 16:29:13 -0400 Subject: [PATCH] Fix timing of layout margin layout invalidation for bars --- CHANGELOG.md | 1 + .../BarModel/SwiftUI.View+BarModel.swift | 1 + .../ItemModel/SwiftUI.View+ItemModel.swift | 1 + .../SwiftUI.View+SupplementaryItemModel.swift | 1 + .../SwiftUI/EpoxySwiftUIHostingView.swift | 39 ++++++++++++++++--- 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beebca81..4915a528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 unexpected dimensions - Made new layout-based SwiftUI cell rendering option the default. - Fixed interaction of SwiftUI bars on visionOS +- Added flag for forcing layout on a hosted SwiftUI view after layout margins change ## [0.10.0](https://github.com/airbnb/epoxy-ios/compare/0.9.0...0.10.0) - 2023-06-29 diff --git a/Sources/EpoxyBars/BarModel/SwiftUI.View+BarModel.swift b/Sources/EpoxyBars/BarModel/SwiftUI.View+BarModel.swift index 61ca7376..f89917cb 100644 --- a/Sources/EpoxyBars/BarModel/SwiftUI.View+BarModel.swift +++ b/Sources/EpoxyBars/BarModel/SwiftUI.View+BarModel.swift @@ -21,6 +21,7 @@ extension View { content: .init(rootView: self, dataID: dataID), style: .init( reuseBehavior: reuseBehavior, + forceLayoutOnLayoutMarginsChange: true, initialContent: .init(rootView: self, dataID: dataID))) .linkDisplayLifecycle() } diff --git a/Sources/EpoxyCollectionView/Models/ItemModel/SwiftUI.View+ItemModel.swift b/Sources/EpoxyCollectionView/Models/ItemModel/SwiftUI.View+ItemModel.swift index 1aa6f70a..2d1632b6 100644 --- a/Sources/EpoxyCollectionView/Models/ItemModel/SwiftUI.View+ItemModel.swift +++ b/Sources/EpoxyCollectionView/Models/ItemModel/SwiftUI.View+ItemModel.swift @@ -21,6 +21,7 @@ extension View { content: .init(rootView: self, dataID: dataID), style: .init( reuseBehavior: reuseBehavior, + forceLayoutOnLayoutMarginsChange: false, initialContent: .init(rootView: self, dataID: dataID))) .linkDisplayLifecycle() } diff --git a/Sources/EpoxyCollectionView/Models/SupplementaryItemModel/SwiftUI.View+SupplementaryItemModel.swift b/Sources/EpoxyCollectionView/Models/SupplementaryItemModel/SwiftUI.View+SupplementaryItemModel.swift index 9b5413d8..a2901b16 100644 --- a/Sources/EpoxyCollectionView/Models/SupplementaryItemModel/SwiftUI.View+SupplementaryItemModel.swift +++ b/Sources/EpoxyCollectionView/Models/SupplementaryItemModel/SwiftUI.View+SupplementaryItemModel.swift @@ -21,6 +21,7 @@ extension View { content: .init(rootView: self, dataID: dataID), style: .init( reuseBehavior: reuseBehavior, + forceLayoutOnLayoutMarginsChange: false, initialContent: .init(rootView: self, dataID: dataID))) .linkDisplayLifecycle() } diff --git a/Sources/EpoxyCore/SwiftUI/EpoxySwiftUIHostingView.swift b/Sources/EpoxyCore/SwiftUI/EpoxySwiftUIHostingView.swift index d7ab1933..63efb847 100644 --- a/Sources/EpoxyCore/SwiftUI/EpoxySwiftUIHostingView.swift +++ b/Sources/EpoxyCore/SwiftUI/EpoxySwiftUIHostingView.swift @@ -70,6 +70,7 @@ public final class EpoxySwiftUIHostingView: UIView, EpoxyableVie ignoreSafeArea: true) dataID = style.initialContent.dataID ?? DefaultDataID.noneProvided as AnyHashable + forceLayoutOnLayoutMarginsChange = style.forceLayoutOnLayoutMarginsChange super.init(frame: .zero) @@ -97,20 +98,33 @@ public final class EpoxySwiftUIHostingView: UIView, EpoxyableVie // MARK: Public public struct Style: Hashable { - public init(reuseBehavior: SwiftUIHostingViewReuseBehavior, initialContent: Content) { + + // MARK: Lifecycle + + public init( + reuseBehavior: SwiftUIHostingViewReuseBehavior, + forceLayoutOnLayoutMarginsChange: Bool, + initialContent: Content) + { self.reuseBehavior = reuseBehavior + self.forceLayoutOnLayoutMarginsChange = forceLayoutOnLayoutMarginsChange self.initialContent = initialContent } + // MARK: Public + public var reuseBehavior: SwiftUIHostingViewReuseBehavior + public var forceLayoutOnLayoutMarginsChange: Bool public var initialContent: Content public static func == (lhs: Style, rhs: Style) -> Bool { - lhs.reuseBehavior == rhs.reuseBehavior + lhs.reuseBehavior == rhs.reuseBehavior && + lhs.forceLayoutOnLayoutMarginsChange == rhs.forceLayoutOnLayoutMarginsChange } public func hash(into hasher: inout Hasher) { hasher.combine(reuseBehavior) + hasher.combine(forceLayoutOnLayoutMarginsChange) } } @@ -212,10 +226,22 @@ public final class EpoxySwiftUIHostingView: UIView, EpoxyableVie trailing: margins.right) } - // Allow the layout margins update to fully propagate through to the SwiftUI View before - // invalidating the layout. - DispatchQueue.main.async { - self.viewController.view.invalidateIntrinsicContentSize() + if forceLayoutOnLayoutMarginsChange { + // If we don't force a layout pass and size invalidation synchronously after the layout + // margins change, it's possible for the hosting view to render with incorrect margins, + // causing a visual jump as the layout resolves over multiple runloop iterations. This seems + // to be more common with top and bottom bars, since they can be laid out early during view + // controller transitions. If this works well, we may make this the default behavior for all + // SwiftUI views. + viewController.view.setNeedsLayout() + viewController.view.layoutIfNeeded() + viewController.view.invalidateIntrinsicContentSize() + } else { + // Allow the layout margins update to fully propagate through to the SwiftUI View before + // invalidating the layout. + DispatchQueue.main.async { + self.viewController.view.invalidateIntrinsicContentSize() + } } } @@ -236,6 +262,7 @@ public final class EpoxySwiftUIHostingView: UIView, EpoxyableVie private let viewController: EpoxySwiftUIHostingController> private let epoxyContent: EpoxyHostingContent private let epoxyEnvironment = EpoxyHostingEnvironment() + private let forceLayoutOnLayoutMarginsChange: Bool private var dataID: AnyHashable private var state: AppearanceState = .disappeared