diff --git a/ios/Extensions.swift b/ios/Extensions.swift index 22641a82..b7130d84 100644 --- a/ios/Extensions.swift +++ b/ios/Extensions.swift @@ -4,19 +4,65 @@ import UIKit /** Helper used to render UIView inside of SwiftUI. + Uses UIViewControllerRepresentable to re-inject safe area insets + that SwiftUI's .ignoresSafeArea() strips from child UIKit views. */ -struct RepresentableView: UIViewRepresentable { +struct RepresentableView: UIViewControllerRepresentable { var view: UIView - // Adding a wrapper UIView to avoid SwiftUI directly managing React Native views. - // This fixes issues with incorrect layout rendering. - func makeUIView(context: Context) -> UIView { - let wrapper = UIView() - wrapper.addSubview(view) - return wrapper + func makeUIViewController(context: Context) -> PageChildViewController { + let viewController = PageChildViewController() + viewController.wrappedView = view + return viewController } - func updateUIView(_ uiView: UIView, context: Context) {} + func updateUIViewController(_ uiViewController: PageChildViewController, context: Context) {} +} + +class PageChildViewController: UIViewController { + var wrappedView: UIView? + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .clear + if let wrappedView { + view.addSubview(wrappedView) + } + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + propagateSafeArea() + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + propagateSafeArea() + } + + /// Re-applies safe area insets from a stable UIKit source, since SwiftUI's + /// .ignoresSafeArea() causes embedded UIKit views to report .zero. + private func propagateSafeArea() { + let insets = nearestNonZeroSafeAreaInsets() ?? view.window?.safeAreaInsets ?? .zero + if abs(additionalSafeAreaInsets.top - insets.top) > 0.5 + || abs(additionalSafeAreaInsets.left - insets.left) > 0.5 + || abs(additionalSafeAreaInsets.bottom - insets.bottom) > 0.5 + || abs(additionalSafeAreaInsets.right - insets.right) > 0.5 { + additionalSafeAreaInsets = insets + } + } + + private func nearestNonZeroSafeAreaInsets() -> UIEdgeInsets? { + var current = view.superview + while let candidate = current { + let insets = candidate.safeAreaInsets + if insets.top > 0 || insets.left > 0 || insets.bottom > 0 || insets.right > 0 { + return insets + } + current = candidate.superview + } + return nil + } } extension Collection { @@ -71,4 +117,3 @@ extension UIHostingController { } } } - diff --git a/ios/PagerView.swift b/ios/PagerView.swift index 58d97882..e3852c23 100644 --- a/ios/PagerView.swift +++ b/ios/PagerView.swift @@ -18,6 +18,7 @@ struct PagerView: View { .id(props.children.count) .background(.clear) .tabViewStyle(.page(indexDisplayMode: .never)) + .ignoresSafeArea() .environment(\.layoutDirection, props.layoutDirection.converted) .introspect(.tabView(style: .page), on: .iOS(.v14...)) { collectionView in self.collectionView = collectionView diff --git a/ios/PagerViewProvider.swift b/ios/PagerViewProvider.swift index 1b1e8754..6457b0a5 100644 --- a/ios/PagerViewProvider.swift +++ b/ios/PagerViewProvider.swift @@ -113,22 +113,12 @@ import UIKit return } - // Only create the hosting controller once it can actually be attached to the - // React view-controller hierarchy. `reactViewController()` can be nil on the - // first `didMoveToWindow` pass (e.g. when the pager is mounted inside a screen - // that is still being attached). Previously `self.hostingController` was - // assigned before this check, so when `reactViewController()` was nil the - // SwiftUI host was never added/pinned (its view frame stayed `.zero`, leaving - // the page blank) while the early-return above prevented any later retry. - // Deferring keeps `hostingController` nil so a subsequent window/layout pass - // can attach it once the view controller is available. guard let parentViewController = reactViewController() else { return } let hostingController = UIHostingController( - rootView: PagerView(props: props, delegate: delegate), - ignoreSafeArea: true + rootView: PagerView(props: props, delegate: delegate) ) self.hostingController = hostingController