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 timing of layout margin layout invalidation for bars #168

Merged
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 @@ -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
bryankeller marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading