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

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. The workaround lives in its own file and every entry point returns early on iOS 16+, so deleting the file and its single call site in PagerViewProvider.swift is all that is needed once iOS 15 support is dropped.
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.

1 participant