Skip to content

Commit

Permalink
Fix timing of layout margin layout invalidation for bars (#168)
Browse files Browse the repository at this point in the history
* Fix timing of layout margin layout invalidation for bars

* Update CHANGELOG.md

Co-authored-by: Bryn Bodayle <[email protected]>

* Update EpoxySwiftUIHostingView.swift

---------

Co-authored-by: Bryn Bodayle <[email protected]>
  • Loading branch information
bryankeller and brynbodayle committed Apr 23, 2024
1 parent 688aed5 commit 158193d
Show file tree
Hide file tree
Showing 5 changed files with 37 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions Sources/EpoxyBars/BarModel/SwiftUI.View+BarModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
39 changes: 33 additions & 6 deletions Sources/EpoxyCore/SwiftUI/EpoxySwiftUIHostingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public final class EpoxySwiftUIHostingView<RootView: View>: UIView, EpoxyableVie
ignoreSafeArea: true)

dataID = style.initialContent.dataID ?? DefaultDataID.noneProvided as AnyHashable
forceLayoutOnLayoutMarginsChange = style.forceLayoutOnLayoutMarginsChange

super.init(frame: .zero)

Expand Down Expand Up @@ -97,20 +98,33 @@ public final class EpoxySwiftUIHostingView<RootView: View>: 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)
}
}

Expand Down Expand Up @@ -212,10 +226,22 @@ public final class EpoxySwiftUIHostingView<RootView: View>: 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()
}
}
}

Expand All @@ -236,6 +262,7 @@ public final class EpoxySwiftUIHostingView<RootView: View>: UIView, EpoxyableVie
private let viewController: EpoxySwiftUIHostingController<EpoxyHostingWrapper<RootView>>
private let epoxyContent: EpoxyHostingContent<RootView>
private let epoxyEnvironment = EpoxyHostingEnvironment()
private let forceLayoutOnLayoutMarginsChange: Bool
private var dataID: AnyHashable
private var state: AppearanceState = .disappeared

Expand Down

0 comments on commit 158193d

Please sign in to comment.