Skip to content

fix(ios): propagate safe area insets to child UIKit views inside PagerView#1085

Merged
troZee merged 3 commits into
callstack:masterfrom
IsaacIsrael:fix/ios-safe-area-propagation
Jul 10, 2026
Merged

fix(ios): propagate safe area insets to child UIKit views inside PagerView#1085
troZee merged 3 commits into
callstack:masterfrom
IsaacIsrael:fix/ios-safe-area-propagation

Conversation

@IsaacIsrael

@IsaacIsrael IsaacIsrael commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #1090 #1096

Problem

When using react-native-pager-view inside a screen with iOS safe area (e.g. behind a translucent tab bar on iOS 26 liquid glass), child UIKit views — including UIScrollView / FlashList — receive safeAreaInsets = .zero. This causes contentInsetAdjustmentBehavior to have no effect: scroll views cannot automatically adjust their content insets to account for overlapping system chrome.

Root cause

The current implementation uses UIViewRepresentable to embed React Native views inside SwiftUI pages. The UIHostingController is initialized with ignoreSafeArea: true, which dynamically subclasses the hosting view to override safeAreaInsets.zero. Even without ignoreSafeArea, SwiftUI's layout system does not propagate safe area insets to child UIKit views embedded via UIViewRepresentable.

Debug evidence (from native logging added during investigation):

HostingController.view.safeAreaInsets: bottom: 83     ← correct
PagingCollectionView.safeAreaInsets:   bottom: 83     ← correct
Wrapper UIView.safeAreaInsets:         bottom: 0      ← LOST
RCTEnhancedScrollView.safeAreaInsets:  bottom: 0      ← zero → contentInsetAdjustmentBehavior has nothing to adjust

Solution

Three targeted changes:

  1. Extensions.swift — 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.

  2. PagerView.swift — Add .ignoresSafeArea() to the SwiftUI TabView body so page content extends visually behind system chrome (translucent tab bars, etc.), enabling the "scroll behind glass" effect.

  3. PagerViewProvider.swift — Remove ignoreSafeArea: true from UIHostingController init so the hosting view preserves real device insets for PageChildViewController to read.

How it works

HostingController.view (bottom: 83)  ← real insets preserved (no more ignoreSafeArea)
  → PagingCollectionView (bottom: 83)
    → [.ignoresSafeArea() — content extends behind tab bar]
      → PageChildViewController
        → additionalSafeAreaInsets = hosting.safeAreaInsets  ← RE-INJECTED
          → Child UIKit views (bottom: 83)  ← contentInsetAdjustmentBehavior works!

The guard with 0.5pt threshold prevents infinite layout loops when viewDidLayoutSubviews sets additionalSafeAreaInsets.

Test plan

  • Verify contentInsetAdjustmentBehavior='scrollableAxes' works on scroll views inside PagerView pages (last item should stop above the tab bar when scrolled to bottom)
  • Verify content scrolls behind translucent tab bar (liquid glass effect on iOS 26)
  • Verify no layout regression on non-liquid-glass screens (pre-iOS 26)
  • Verify horizontal paging still works correctly
  • Verify no infinite layout loops (check CPU usage)

Made with Cursor

@troZee

troZee commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

@IsaacIsrael
Could you fix the conflicts?

…rView

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 <cursoragent@cursor.com>
@IsaacIsrael IsaacIsrael force-pushed the fix/ios-safe-area-propagation branch from cc96767 to 0d8982c Compare July 6, 2026 10:02
@IsaacIsrael

Copy link
Copy Markdown
Contributor Author

@troZee done :)

Comment thread ios/Extensions.swift Outdated
private func propagateHostingSafeArea() {
var current: UIView? = view.superview
while let v = current {
if String(describing: type(of: v)).contains("_UIHostingView") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add an else statement and log this error if _UIHostingView is not found?

  - Removed the private _UIHostingView class-name lookup.
  - Added viewSafeAreaInsetsDidChange() so updates happen when UIKit safe-area values change.
  - propagateSafeArea() now uses the nearest UIKit superview with non-zero safeAreaInsets, falling back to
    view.window?.safeAreaInsets.
@troZee

troZee commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@IsaacIsrael
I added a different code, but I was not able to test it. Could you check this code and provide a repro code?

In my PR, I applied the safer safe-area propagation in ios/Extensions.swift.

What changed:

  • Removed the private _UIHostingView class-name lookup.
  • Added viewSafeAreaInsetsDidChange() so updates happen when UIKit safe-area values change.
  • propagateSafeArea() now uses the nearest UIKit superview with non-zero safeAreaInsets, falling back to
    view.window?.safeAreaInsets.

@troZee

troZee commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

@wonhyunkwon Could you test this PR?

@wonhyunkwon

Copy link
Copy Markdown

@troZee
Tested this PR against our production app and it fixes the exact issue I reported in #1097. 👍

How I applied it
Applied the changes on top of the 8.0.0 release via a pnpm patch. Note the PagerViewProvider.swift hunk didn't apply as-is since the guard let parentViewController refactor only exists on main — on 8.0.0 the equivalent change is just removing the ignoreSafeArea: true argument from the UIHostingController init. Extensions.swift and PagerView.swift applied cleanly.

Environment

  • react-native-pager-view 8.0.0 + this PR's changes
  • React Native 0.83.4 (new architecture), Expo SDK 55, @gorhom/bottom-sheet 5.2.6
  • Physical iPhone 15 Pro (iOS 26.5) — this bug only reproduces on a physical device; the simulator's SwiftUI keyboard avoidance behaves differently, so simulator testing won't catch it

Scenario
PagerView rendered inside a @gorhom/bottom-sheet with a BottomSheetTextInput above it.

  • Before: focusing the input triggers SwiftUI TabView's automatic keyboard avoidance, shrinking the pager and leaving a blank gap above the keyboard (RN layout values stay correct — it's purely native-level)
  • After: the pager keeps its RN-driven layout when the keyboard appears — no gap ✅

Also checked our other PagerView usages (comment bottom sheet) and saw no safe-area regressions from the .ignoresSafeArea() + inset re-injection approach.

As for #1097, I'll leave it open for now and defer to the maintainers on whether to close it in favor of this PR — feel free to close it if this one moves forward.

@troZee troZee merged commit 764eeab into callstack:master Jul 10, 2026
2 checks passed
@troZee

troZee commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

@IsaacIsrael IsaacIsrael deleted the fix/ios-safe-area-propagation branch July 10, 2026 12:26
@IsaacIsrael

Copy link
Copy Markdown
Contributor Author

Sorry @troZee , I missed your message and thanks @wonhyunkwon for testing :)

@troZee

troZee commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

@IsaacIsrael could you test the newest version of it works for you? 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] New Arch: safe area insets are stripped from PagerView children

3 participants