Skip to content

fix(ios): keep a hidden navigation bar hidden on iOS 15 - #1190

Open
zewillze wants to merge 1 commit into
callstack:mainfrom
zewillze:fix/ios-15-navigation-bar-hidden
Open

zewillze wants to merge 1 commit into
callstack:mainfrom
zewillze:fix/ios-15-navigation-bar-hidden

Conversation

@zewillze

Copy link
Copy Markdown
Contributor

Summary

Fixes the iOS 15 case where the React Navigation header buttons stop responding after a screen is pushed and popped.

Root cause. On iOS 15 UIHostingController installs SwiftUI's UIKitNavigationBarBridge. Whenever the hosting view appears, the bridge calls -[UINavigationController setNavigationBarHidden:NO] on the enclosing navigation controller. When that bar is deliberately hidden — e.g. the RNSContainerNavigationController that react-native-screens creates for hasTwoStates tab/drawer containers — the bridge re-shows an empty UINavigationBar on top of the screen. It lands exactly over the JS header (frame (0 47; 428 44) in the repro) and swallows every touch there, so the header buttons never fire. It keeps that state after the pop, so the buttons stay dead.

Measured with lldb on an iOS 15.2 simulator after push → pop:

UINavigationBar in the window hitTest on the header button
before present at (0 47; 428 44), hidden = NO _UINavigationBarContentView (not the button)
after none visible the React Native Button view

How it was implemented. The workaround lives in its own file, ios/PagerViewIOS15NavigationBarCompat.swift. Its single entry point starts with if #available(iOS 16, *) and returns a plain UIHostingController there; on iOS 15 and below it returns a UIHostingController subclass that restores the enclosing navigation bar's hidden state around viewWillAppear. PagerViewProvider.swift only changes how the hosting controller is constructed (one call site).

Removal. Once the minimum supported iOS version is 16, delete ios/PagerViewIOS15NavigationBarCompat.swift and replace that single call site with UIHostingController(...). Nothing else references it, and on iOS 16+ the file already returns a plain hosting controller.

  • Impact: iOS only. No API, JS or typing change; only the UIHostingController construction in PagerViewProvider goes through the new factory.
  • Same isolation pattern as the iOS 15 scrollEnabled fix (fix(ios): respect scrollEnabled on iOS 15 #1148).

Test Plan

What's required for testing (prerequisites)?

  • An iOS 15.x simulator or device. Not reproducible on iOS 16+, where SwiftUI no longer re-shows the bar.
  • A stack screen with a React Navigation header that contains the iOS 15 pager, because the bug needs the screen's view controller to be re-attached on pop. The repro project is https://github.com/zewillze/react-native-pager-view-ios15-issue — Home is a @react-navigation/stack screen with headerLeft/headerRight, Home contains a createMaterialTopTabNavigator, and the root navigator is createBottomTabNavigator.

What are the steps to reproduce?

  1. Run the repro on an iOS 15.x simulator.
  2. Tap a list row (pushes Detail inside the stack).
  3. Go back.
  4. Tap either header button ("Button" or "+").
    • Before: nothing happens — the tap is consumed by the re-shown, empty UINavigationBar.
    • After: the Alert shows.

Optional check without touches, after step 3 — dump the window hierarchy (po [[[UIApplication sharedApplication] keyWindow] performSelector:@selector(recursiveDescription)]):

  • Before: a UINavigationBar at (0 47; 428 44) with hidden = NO.
  • After: none.

On the fixed build the pager's hosting controller class is the compat subclass
(PreservingNavigationBarHostingController<PagerView>), which also confirms the #available gate took the iOS 15 path.

Compatibility

OS Implemented
iOS ✅
Android ❌

Android does not have this code path and is unaffected.

Checklist

  • I have tested this on a device and a simulator
  • I added the documentation in README.md
  • I updated the typed files (TS and Flow)

Fixes #1180

@ArekChr ArekChr left a comment

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.

@zewillze Thanks for the fix! There is one issue to fix before merging.
SwiftPM support (#1187) is now merged into main. Please update this branch and add PagerViewIOS15NavigationBarCompat.swift to the source files in ios/Package.swift. Without it, the SwiftPM build will fail.
Please also check that a native navigation bar that should be visible stays visible after opening a screen and going back on iOS 15. The current CI tests run on iOS 26.4, so they do not test this fix.

@ArekChr ArekChr self-assigned this Oct 22, 2026
On iOS 15 SwiftUI's UIHostingController installs UIKitNavigationBarBridge,
which calls -[UINavigationController setNavigationBarHidden:NO] on the
enclosing navigation controller whenever the hosting view appears. When that
bar is deliberately hidden - e.g. the container react-native-screens creates
for hasTwoStates tab/drawer navigators - the bridge re-shows an empty
UINavigationBar, which then swallows touches meant for the JS header buttons
underneath.

Wrap the hosting controller so it restores the previous hidden state around
viewWillAppear. A bar that is already visible is left untouched, so native
navigation bars keep working.

The workaround lives in its own file and every entry point returns early on
iOS 16+, so deleting the file (and its entry in Package.swift) together with
its single call site in PagerViewProvider.swift is all that is needed once
iOS 15 support is dropped.
@zewillze
zewillze force-pushed the fix/ios-15-navigation-bar-hidden branch from 6fac40a to 7726a61 Compare October 23, 2026 13:19
@zewillze

Copy link
Copy Markdown
Contributor Author

hi @ArekChr
1. Rebased onto main with SwiftPM (#1187)

The branch is rebased onto main (a06349e) and the new file is now in the RNCPagerViewSwift target's
sources in ios/Package.swift:

sources: [
    "Extensions.swift",
    "PagerScrollDelegate.swift",
    "PagerView.swift",
    "PagerViewIOS15Compat.swift",
    "PagerViewIOS15NavigationBarCompat.swift",
    "PagerViewProps.swift",
    "PagerViewProvider.swift",
],

I checked the list against every runtime Swift file in ios/ (Package.swift itself excluded) — nothing
is missing. CocoaPods keeps picking the file up from the podspec glob, as before. I could not run the
SwiftPM app build locally (it needs the generated autolinking tree / prebuilt RN xcframeworks), so that
part will be exercised by the SwiftPM CI job.

2. Visible native navigation bar (iOS 15.2)

Agreed that CI (iOS 26.4) does not cover this. The fix only re-hides a bar that was hidden before
viewWillAppear, so a bar that is already visible must be left untouched. I verified it manually on an
iOS 15.2 simulator using a native-stack variant of the repro (so the pager's enclosing
UINavigationController has a visible bar), then pushed Detail and went back:

bar frame hidden alpha
native-stack header (must stay visible) (0 47; 428 44) NO 1
bottom-tabs container bar (must stay hidden) (0 -91; 428 44) YES —

The pager's hosting controller is PreservingNavigationBarHostingController<PagerView>, confirming the
iOS 15 path is active. The original bug is still covered in the @react-navigation/stack variant: after
push → pop there is no visible UINavigationBar in the window (previously one appeared at
(0 47; 428 44) with hidden = NO, and hitTest on the header button returned
_UINavigationBarContentView).

The branch is updated: 3 files, +72/−1 (ios/Package.swift +1, the new compat file +68,
PagerViewProvider.swift +3/−1).

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.

2 participants