From 0d8982cfa1ffc8e75dc6a3eff373f27ec6f60358 Mon Sep 17 00:00:00 2001 From: Isaac Israel Date: Sun, 21 Jun 2026 14:00:40 +0300 Subject: [PATCH] fix(ios): propagate safe area insets to child UIKit views inside PagerView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SwiftUI's `.ignoresSafeArea()` modifier strips `safeAreaInsets` from all child UIKit views embedded via `UIViewRepresentable`. This prevents `contentInsetAdjustmentBehavior` from working on scroll views inside pager pages — the scroll view sees `safeAreaInsets = .zero` even when the hosting controller correctly reports the real device insets (e.g. bottom: 83pt for a tab bar on iOS 26 liquid glass). Fix: replace `UIViewRepresentable` with `UIViewControllerRepresentable`. The new `PageChildViewController` traverses up to `_UIHostingView`, reads its real `safeAreaInsets`, and re-injects them via `additionalSafeAreaInsets`. This restores proper safe area propagation to all descendant UIKit views (including `UIScrollView`), so `contentInsetAdjustmentBehavior` works as expected. Also adds `.ignoresSafeArea()` to the SwiftUI `TabView` body so page content extends visually behind system chrome (tab bars, etc.), and removes the `ignoreSafeArea: true` parameter from `UIHostingController` init so the hosting view preserves real device insets for `PageChildViewController` to read. Co-authored-by: Cursor --- ios/Extensions.swift | 57 +++++++++++++++++++++++++++++++------ ios/PagerView.swift | 1 + ios/PagerViewProvider.swift | 12 +------- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/ios/Extensions.swift b/ios/Extensions.swift index 22641a82..479b1015 100644 --- a/ios/Extensions.swift +++ b/ios/Extensions.swift @@ -4,19 +4,60 @@ 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() + propagateHostingSafeArea() + } + + /// Traverses up to the _UIHostingView and re-applies its bottom safe area + /// as additionalSafeAreaInsets on this child VC, since .ignoresSafeArea() + /// causes UIKit to report safeAreaInsets = .zero for embedded views. + private func propagateHostingSafeArea() { + var current: UIView? = view.superview + while let v = current { + if String(describing: type(of: v)).contains("_UIHostingView") { + let hosting = v.safeAreaInsets + let delta = UIEdgeInsets( + top: hosting.top, left: hosting.left, + bottom: hosting.bottom, right: hosting.right + ) + if abs(additionalSafeAreaInsets.top - delta.top) > 0.5 + || abs(additionalSafeAreaInsets.left - delta.left) > 0.5 + || abs(additionalSafeAreaInsets.bottom - delta.bottom) > 0.5 + || abs(additionalSafeAreaInsets.right - delta.right) > 0.5 { + additionalSafeAreaInsets = delta + } + return + } + current = v.superview + } + } } extension Collection { 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