From 7726a61efb19c0fcdbd6eaf466e29972725f61da Mon Sep 17 00:00:00 2001 From: zewillze Date: Fri, 23 Oct 2026 20:31:49 +0800 Subject: [PATCH] fix(ios): keep a hidden navigation bar hidden on iOS 15 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. --- ios/Package.swift | 1 + ios/PagerViewIOS15NavigationBarCompat.swift | 68 +++++++++++++++++++++ ios/PagerViewProvider.swift | 4 +- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 ios/PagerViewIOS15NavigationBarCompat.swift diff --git a/ios/Package.swift b/ios/Package.swift index 25d09711..f62c5510 100644 --- a/ios/Package.swift +++ b/ios/Package.swift @@ -52,6 +52,7 @@ let package = Package( "PagerScrollDelegate.swift", "PagerView.swift", "PagerViewIOS15Compat.swift", + "PagerViewIOS15NavigationBarCompat.swift", "PagerViewProps.swift", "PagerViewProvider.swift", ], diff --git a/ios/PagerViewIOS15NavigationBarCompat.swift b/ios/PagerViewIOS15NavigationBarCompat.swift new file mode 100644 index 00000000..2a99074e --- /dev/null +++ b/ios/PagerViewIOS15NavigationBarCompat.swift @@ -0,0 +1,68 @@ +import SwiftUI +import UIKit + +/// Workaround for SwiftUI re-showing an intentionally hidden `UINavigationBar` on iOS 15. +/// +/// On iOS 15 `UIHostingController` installs SwiftUI's `UIKitNavigationBarBridge`, which calls +/// `-[UINavigationController setNavigationBarHidden:NO]` on the enclosing navigation controller +/// whenever the hosting view appears. When that bar is deliberately hidden — for example the +/// `RNSContainerNavigationController` that `react-native-screens` creates for `hasTwoStates` +/// tab/drawer containers — the bridge re-shows an empty `UINavigationBar` that then swallows +/// touches meant for the React Native views underneath (in this case the JS header buttons). +/// +/// A navigation bar that is already visible is left untouched, so native headers keep working. +/// +/// On iOS 16 and later SwiftUI no longer re-shows the bar, so a plain `UIHostingController` is +/// returned and none of the code below runs. +/// +/// - Note: Delete this file together with the single `PagerViewIOS15NavigationBarCompat` call in +/// `PagerViewProvider.swift` (and its entry in `Package.swift`) once the minimum supported iOS +/// version is 16. +enum PagerViewIOS15NavigationBarCompat { + /// Returns a hosting controller that keeps the enclosing navigation bar's hidden state across + /// the appearance callbacks on iOS 15, and a plain `UIHostingController` on iOS 16+. + static func makeHostingController( + rootView: Content, + ignoreSafeArea: Bool + ) -> UIHostingController { + if #available(iOS 16, *) { + return UIHostingController(rootView: rootView, ignoreSafeArea: ignoreSafeArea) + } + + let hostingController = PreservingNavigationBarHostingController(rootView: rootView) + if ignoreSafeArea { + hostingController.disableSafeArea() + } + + return hostingController + } +} + +/// `UIHostingController` that restores the enclosing navigation bar's hidden state around +/// `viewWillAppear`, see `PagerViewIOS15NavigationBarCompat`. +private final class PreservingNavigationBarHostingController: UIHostingController { + override func viewWillAppear(_ animated: Bool) { + let navigationController = enclosingNavigationController + let wasNavigationBarHidden = navigationController?.isNavigationBarHidden == true + super.viewWillAppear(animated) + if wasNavigationBarHidden { + navigationController?.setNavigationBarHidden(true, animated: false) + } + } + + private var enclosingNavigationController: UINavigationController? { + if let navigationController { + return navigationController + } + + var viewController: UIViewController? = self + while let current = viewController { + if let navigationController = current as? UINavigationController { + return navigationController + } + viewController = current.parent + } + + return nil + } +} diff --git a/ios/PagerViewProvider.swift b/ios/PagerViewProvider.swift index 802da979..87def2ac 100644 --- a/ios/PagerViewProvider.swift +++ b/ios/PagerViewProvider.swift @@ -222,7 +222,9 @@ final class PagerViewProvider: UIView, RNCPagerViewProviding { // has them at full size. `PageChildViewController` reads *this* view's // insets (the RN screen's, including a native search bar) and re-injects // them; the two are independent. - let hostingController = UIHostingController( + // iOS 15 only, see PagerViewIOS15NavigationBarCompat.swift + // (delete together with the iOS 15 support). + let hostingController = PagerViewIOS15NavigationBarCompat.makeHostingController( rootView: PagerView(props: props, delegate: delegate), ignoreSafeArea: true )