Skip to content

Commit 842dcf4

Browse files
authored
Fix iOS JS status bar being ignored until the lightbox opens (#12339)
1 parent 226c0bf commit 842dcf4

5 files changed

Lines changed: 141 additions & 20 deletions

‎patches/react-native-pager-view@9.0.11.patch‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,37 @@ index 56ec98a3be31a328d320dfde3237cb4a058af1f6..015bebfbf0bea5a6eefc4357984e1697
2626
}
2727

2828
if scrollDelegate.originalDelegate == nil {
29+
diff --git a/ios/PagerViewProvider.swift b/ios/PagerViewProvider.swift
30+
index 87def2acf1bfb83fa79ff34a1537cac6afee3752..1e97045531befc80114f7c6f5959a99cbcd05ebd 100644
31+
--- a/ios/PagerViewProvider.swift
32+
+++ b/ios/PagerViewProvider.swift
33+
@@ -74,6 +74,8 @@ final class PagerViewProvider: UIView, RNCPagerViewProviding {
34+
super.didMoveToWindow()
35+
if window != nil {
36+
setupView()
37+
+ } else {
38+
+ detachFromParentViewController()
39+
}
40+
}
41+
42+
@@ -240,6 +242,20 @@ final class PagerViewProvider: UIView, RNCPagerViewProviding {
43+
hostingController.didMove(toParent: parentViewController)
44+
}
45+
46+
+ /// Off-window, the hosting controller must not stay a child of the view
47+
+ /// controller it was attached to. Otherwise an unmounted (or recycled) pager
48+
+ /// leaves it behind, often on the root view controller, which changes what
49+
+ /// UIKit and libraries that inspect `children` see (for example the status
50+
+ /// bar delegation in react-native-screens). It is re-attached by
51+
+ /// `willMove(toWindow:)` / `setupView()` when the pager is back in a window.
52+
+ private func detachFromParentViewController() {
53+
+ guard let hostingController, hostingController.parent != nil else {
54+
+ return
55+
+ }
56+
+ hostingController.willMove(toParent: nil)
57+
+ hostingController.removeFromParent()
58+
+ }
59+
+
60+
/// SwiftUI can recreate the page controller around this React view while
61+
/// preserving the view itself. Keep the hosting controller attached to the
62+
/// current page controller so UIKit's view-controller hierarchy stays valid.

‎patches/react-native-pager-view@9.0.11.patch.md‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# react-native-pager-view+9.0.11.patch
22

3-
Carries one iOS change, in `ios/PagerView.swift`. Drop it once a release
3+
Carries two iOS changes. Drop each one once a release
44
contains it.
55

66
These upstream PRs were carried here until they shipped:
@@ -32,3 +32,21 @@ This can't be fixed from JS. react-native-screens re-applies the option only
3232
when the screen attaches or the prop changes, and RN views do not trigger
3333
UIKit's edge effect. Drop this part if upstream adds a prop to keep the top
3434
edge effect.
35+
36+
## iOS: detach the hosting controller when the pager leaves the window
37+
38+
Backport of the `didMoveToWindow` hunk of upstream PR
39+
https://github.com/callstack/react-native-pager-view/pull/1204 (released in
40+
10.0.0). Drop it when we upgrade to 10.x.
41+
42+
In 9.0.11 the pager's `UIHostingController` is added as a child of
43+
`reactViewController()` and never removed, only re-parented. The lightbox
44+
pager sits outside the navigation stack, so after the first lightbox open the
45+
root view controller keeps a leaked hosting controller as its last child.
46+
47+
That matters because react-native-screens' status bar swizzle only delegates to
48+
its own view controllers when one is the root's *last* child. The leak changed
49+
which view controller UIKit asked for the status bar style for the rest of the
50+
session (it made the JS status bar start working, and would make RNS screen
51+
status bar options stop working). With the detach, the root's children no
52+
longer depend on whether the lightbox has been opened.

‎patches/react-native-screens@4.30.1.patch‎

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ index 0000000000000000000000000000000000000000..7498c7aea5a853ad12ea607cce921d47
470470
+
471471
+} // namespace rnscreens
472472
diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm
473-
index 049fd22427b2bde17fd7ca59b8de65d0ee07b4b5..ac484f9033e16f61fb382ed266eac8c3d6fe8b74 100644
473+
index 049fd22427b2bde17fd7ca59b8de65d0ee07b4b5..a1597db71b69de1c2db87ddf9f825aa51850f2e7 100644
474474
--- a/ios/RNSScreen.mm
475475
+++ b/ios/RNSScreen.mm
476476
@@ -1,4 +1,5 @@
@@ -479,7 +479,15 @@ index 049fd22427b2bde17fd7ca59b8de65d0ee07b4b5..ac484f9033e16f61fb382ed266eac8c3
479479

480480
#import "RNSModalScreen.h"
481481
#import "RNSScreen.h"
482-
@@ -1226,9 +1227,82 @@ - (BOOL)hasHeaderConfig
482+
@@ -10,6 +11,7 @@
483+
#import <React/RCTFabricComponentsPlugins.h>
484+
#import <React/RCTRootComponentView.h>
485+
#import <React/RCTScrollViewComponentView.h>
486+
+#import <React/RCTStatusBarAppearance.h>
487+
#import <React/RCTSurfaceTouchHandler.h>
488+
#import <react/renderer/components/rnscreens/EventEmitters.h>
489+
#import <react/renderer/components/rnscreens/Props.h>
490+
@@ -1226,9 +1228,82 @@ - (BOOL)hasHeaderConfig
483491
return _config != nil;
484492
}
485493

@@ -562,7 +570,39 @@ index 049fd22427b2bde17fd7ca59b8de65d0ee07b4b5..ac484f9033e16f61fb382ed266eac8c3
562570
}
563571

564572
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
565-
@@ -2110,6 +2184,12 @@ - (RNSOrientation)evaluateOrientation
573+
@@ -1955,7 +2030,8 @@ - (UIViewController *)childViewControllerForStatusBarHidden
574+
575+
- (BOOL)prefersStatusBarHidden
576+
{
577+
- return self.screenView.statusBarHidden;
578+
+ // A screen without the prop defers to the status bar set from JS.
579+
+ return self.screenView.hasStatusBarHiddenSet ? self.screenView.statusBarHidden : RCTStatusBarAppearance.hidden;
580+
}
581+
582+
- (UIViewController *)childViewControllerForStatusBarStyle
583+
@@ -1966,7 +2042,10 @@ - (UIViewController *)childViewControllerForStatusBarStyle
584+
585+
- (UIStatusBarStyle)preferredStatusBarStyle
586+
{
587+
- return [RNSScreenWindowTraits statusBarStyleForRNSStatusBarStyle:self.screenView.statusBarStyle];
588+
+ // A screen without the prop defers to the status bar set from JS.
589+
+ return self.screenView.hasStatusBarStyleSet
590+
+ ? [RNSScreenWindowTraits statusBarStyleForRNSStatusBarStyle:self.screenView.statusBarStyle]
591+
+ : RCTStatusBarAppearance.style;
592+
}
593+
594+
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
595+
@@ -1976,7 +2055,8 @@ - (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
596+
if ([vc isKindOfClass:[RNSScreen class]]) {
597+
return ((RNSScreen *)vc).screenView.statusBarAnimation;
598+
}
599+
- return UIStatusBarAnimationFade;
600+
+ // No screen sets the animation, so use the one set from JS.
601+
+ return RCTStatusBarAppearance.updateAnimation;
602+
}
603+
604+
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
605+
@@ -2110,6 +2190,12 @@ - (RNSOrientation)evaluateOrientation
566606

567607
- (void)addSnapshotToView
568608
{
@@ -575,7 +615,7 @@ index 049fd22427b2bde17fd7ca59b8de65d0ee07b4b5..ac484f9033e16f61fb382ed266eac8c3
575615
// if we dismissed the view natively, it will already be detached from view hierarchy
576616
if (self.view.window == nil) {
577617
return;
578-
@@ -2123,6 +2203,13 @@ - (void)addSnapshotToView
618+
@@ -2123,6 +2209,13 @@ - (void)addSnapshotToView
579619
snapshot.frame = self.view.bounds;
580620
// fill the whole screen view with the snapshot
581621
snapshot.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

‎patches/react-native-screens@4.30.1.patch.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,35 @@ frames are in the screen view's coordinates, which match the snapshot's. They
5757
are now also released when the view has already left the window and no
5858
snapshot is taken. The 4.30.0 port hasn't been re-run against the repro.
5959

60+
## iOS: screens without status bar props defer to the JS status bar
61+
62+
Port of upstream PR
63+
https://github.com/software-mansion/react-native-screens/pull/5135 (open, not
64+
in a release as of 4.30.1).
65+
66+
With `UIViewControllerBasedStatusBarAppearance = YES`, React Native 0.88.1
67+
stores `<StatusBar>` / `StatusBar.pushStackEntry` values in
68+
`RCTStatusBarAppearance` and expects the root view controller to return them
69+
(`ExpoRootViewController` does). But `UIViewController+RNScreens.mm` swizzles
70+
`childViewControllerForStatusBarStyle/Hidden` on every view controller: when
71+
the root's last child is an RNS controller, UIKit asks the top `RNSScreen`
72+
instead, which returned hard-coded defaults (`Auto` style following the system
73+
appearance, not hidden, `Fade`). So every JS status bar change was ignored on
74+
iOS, e.g. the light status bar over profile banners, and the app theme when it
75+
differs from the system appearance.
76+
77+
It only showed from a cold start. Opening the lightbox leaked pager-view's
78+
hosting controller onto the root view controller, so the root's last child was
79+
no longer an RNS controller and the swizzle fell through. See the pager-view
80+
patch. Debug dev-client builds also hide it, because the dev launcher puts a
81+
non-RNS view controller in the chain, so verify in a Release build.
82+
83+
The patch makes `RNSScreen` fall back to `RCTStatusBarAppearance` for each
84+
trait the screen doesn't set. A screen that sets `statusBarStyle` /
85+
`statusBarHidden` / `statusBarAnimation` still wins. We set none of them, so
86+
for us the JS status bar always applies. Verified in a Release build on an
87+
iOS 27.2 simulator (Xcode 27.2): broken before, fixed after.
88+
6089
## iOS + Android: do not dirty shadow nodes' Yoga nodes on every clone
6190

6291
Several component descriptors call `YogaLayoutableShadowNode::setSize()` from

0 commit comments

Comments
 (0)