Skip to content

Commit e8e63a5

Browse files
alanjhughesfacebook-github-bot
authored andcommitted
Reland iOS 27 StatusBar fix with tvOS guards (#58926)
Summary: Fixes #58758. Apps built with the iOS 27 SDK can no longer change the status bar through the `UIApplication` setters. Restore the view-controller-based implementation: `RCTStatusBarAppearance` stores the requested style, hidden state and animation, and the default root and Fabric modal controllers return those values. The modal preserves the appearance captured before presentation until JavaScript changes it. Apps must set `UIViewControllerBasedStatusBarAppearance` to `YES` on iOS 27. Custom root view controllers must return the `RCTStatusBarAppearance` values. Apps using `NO` retain the legacy setters on older SDKs and receive a one-time warning where those setters are ineffective. Exclude the appearance helper and status-bar controller code on tvOS, where UIKit's status-bar types and APIs are unavailable. Guard the modal capture declaration and call as well as its state and implementation. The iOS code paths are unchanged. Test Plan: - Built the full `AirwaveBurbn` app and package for appletvos-arm64 with the Airwave mode, local build flavour, Pika 27 and Xcode 27.1. Code signing was skipped for local validation. Both targets passed. Buck run: https://www.internalfb.com/buck2/9c21f926-63ea-4fd5-8370-286f70fc829b. - Built `ReactApple`, `RCTFabricApple` and `RCTAppDelegateApple` with `AirwavePlatform-appletvos-arm64-local`. All passed. Buck run: https://www.internalfb.com/buck2/0aaabc5f-e893-4d9d-8ce1-a346c3315bbf. - Built `ReactApple`, `CoreModulesApple`, `RCTFabricApple` and `RCTAppDelegateApple` for iphonesimulator-arm64. All passed. Buck run: https://www.internalfb.com/buck2/876de6d7-fbd8-439f-bdbe-d4fed3d29a91. - Regenerated all C++ API snapshots. Only the four ReactApple snapshots changed, each adding `RCTStatusBarAppearance`; no private API appeared. Ran the snapshot target with `--validate`; all 12 snapshots passed. Validation run: https://www.internalfb.com/buck2/a8fc85da-ba96-4b3b-a741-afede29b5f0d. - Meta-managed clang-format, `arc f` and `arc lint` passed. - Independent read-only review found no blockers in the tvOS guards and confirmed that the iOS implementation matches the original landed change apart from platform guards. - Restored the original 12 `RCTStatusBarAppearanceTests` XCTest cases. These remain registered in the OSS Xcode test target, have no internal Buck owner, and were not run locally. No simulator runtime verification was performed. - The manual Sandcastle smoke launcher defaulted to iphoneos rather than appletvos. Its build and baseline retry failed target-compatibility checks before compiling. Do not count the launcher success as a passing tvOS build; the full tvOS validation is the local Buck run above. Child run: https://www.internalfb.com/intern/sandcastle/instance/40532397437851209/. - Diff-time CI currently has a non-bypassable `github-export-checks` failure because this new diff has no linked/exported GitHub PR. Export at https://www.internalfb.com/intern/opensource/export/preview/D127817413/ and rerun the check before landing. Other CI jobs are still running. - General Reviewer flagged a modal-state edge case also present in the original patch: changing a JS status-bar value and then returning to its capture-time value can restore the pre-modal native appearance. That warning remains unresolved for review; this re-land deliberately preserves the original iOS behaviour. Differential Revision: D127817413 Pulled By: cipolleschi
1 parent 7aff712 commit e8e63a5

15 files changed

Lines changed: 428 additions & 15 deletions

File tree

‎packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,34 @@
1414
#endif
1515

1616
#import <RCTAnimatedModuleProvider/RCTAnimatedModuleProvider.h>
17+
#import <React/RCTStatusBarAppearance.h>
1718
#import <react/featureflags/ReactNativeFeatureFlags.h>
1819
#import <react/nativemodule/defaults/DefaultTurboModules.h>
1920

21+
@interface RCTRootViewController : UIViewController
22+
@end
23+
24+
@implementation RCTRootViewController
25+
26+
#if !TARGET_OS_TV
27+
- (UIStatusBarStyle)preferredStatusBarStyle
28+
{
29+
return RCTStatusBarAppearance.style;
30+
}
31+
32+
- (BOOL)prefersStatusBarHidden
33+
{
34+
return RCTStatusBarAppearance.hidden;
35+
}
36+
37+
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
38+
{
39+
return RCTStatusBarAppearance.updateAnimation;
40+
}
41+
#endif
42+
43+
@end
44+
2045
@implementation RCTDefaultReactNativeFactoryDelegate {
2146
// C++ Native Animated provider, created once on first use (getTurboModule: may be called
2247
// concurrently for different module names).
@@ -35,7 +60,7 @@ - (NSURL *_Nullable)sourceURLForBridge:(nonnull RCTBridge *)bridge
3560

3661
- (UIViewController *)createRootViewController
3762
{
38-
return [UIViewController new];
63+
return [RCTRootViewController new];
3964
}
4065

4166
- (void)setRootView:(UIView *)rootView toRootViewController:(UIViewController *)rootViewController

‎packages/react-native/Libraries/AppDelegate/RCTUIConfiguratorProtocol.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ NS_ASSUME_NONNULL_BEGIN
4141

4242
/**
4343
* It creates the RootViewController.
44-
* By default, it creates a new instance of a `UIViewController`.
44+
* By default, it creates a view controller that returns the `RCTStatusBarAppearance` values.
4545
* You can override it to provide your own initial ViewController.
4646
*
4747
* @return: an instance of `UIViewController`.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <UIKit/UIKit.h>
9+
10+
#if !TARGET_OS_TV
11+
NS_ASSUME_NONNULL_BEGIN
12+
13+
/**
14+
* The status bar state set from JavaScript. Root view controllers return these values from their status bar methods.
15+
*/
16+
@interface RCTStatusBarAppearance : NSObject
17+
18+
@property (class, nonatomic, readonly) UIStatusBarStyle style;
19+
@property (class, nonatomic, readonly) BOOL hidden;
20+
@property (class, nonatomic, readonly) UIStatusBarAnimation updateAnimation;
21+
22+
+ (void)setStyle:(UIStatusBarStyle)style animated:(BOOL)animated;
23+
+ (void)setHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation;
24+
25+
@end
26+
27+
NS_ASSUME_NONNULL_END
28+
#endif
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTStatusBarAppearance.h"
9+
10+
#if !TARGET_OS_TV
11+
#import "RCTAssert.h"
12+
#import "RCTUtils.h"
13+
14+
static UIStatusBarStyle sStyle = UIStatusBarStyleDefault;
15+
static BOOL sHidden = NO;
16+
static UIStatusBarAnimation sUpdateAnimation = UIStatusBarAnimationNone;
17+
18+
@implementation RCTStatusBarAppearance
19+
20+
+ (UIStatusBarStyle)style
21+
{
22+
return sStyle;
23+
}
24+
25+
+ (BOOL)hidden
26+
{
27+
return sHidden;
28+
}
29+
30+
+ (UIStatusBarAnimation)updateAnimation
31+
{
32+
return sUpdateAnimation;
33+
}
34+
35+
+ (void)setStyle:(UIStatusBarStyle)style animated:(BOOL)animated
36+
{
37+
RCTAssertMainQueue();
38+
sStyle = style;
39+
[self updateStatusBarAnimated:animated];
40+
}
41+
42+
+ (void)setHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation
43+
{
44+
RCTAssertMainQueue();
45+
sHidden = hidden;
46+
sUpdateAnimation = animation;
47+
[self updateStatusBarAnimated:animation != UIStatusBarAnimationNone];
48+
}
49+
50+
+ (void)updateStatusBarAnimated:(BOOL)animated
51+
{
52+
UIViewController *viewController = RCTPresentedViewController();
53+
if (animated) {
54+
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration
55+
animations:^{
56+
[viewController setNeedsStatusBarAppearanceUpdate];
57+
}];
58+
} else {
59+
[viewController setNeedsStatusBarAppearanceUpdate];
60+
}
61+
}
62+
63+
@end
64+
#endif

‎packages/react-native/React/CoreModules/RCTStatusBarManager.mm‎

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#import <React/RCTEventDispatcherProtocol.h>
1212
#import <React/RCTInitializing.h>
1313
#import <React/RCTLog.h>
14+
#import <React/RCTStatusBarAppearance.h>
1415
#import <React/RCTUtils.h>
1516

1617
#import <FBReactNativeSpec/FBReactNativeSpec.h>
@@ -68,6 +69,28 @@ static BOOL RCTViewControllerBasedStatusBarAppearance()
6869
return value;
6970
}
7071

72+
// UIKit makes the app-level setters no-ops only for apps built with the iOS 27 SDK.
73+
static BOOL RCTAppLevelStatusBarIsNoOp()
74+
{
75+
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 270000
76+
if (@available(iOS 27.0, *)) {
77+
return YES;
78+
}
79+
#endif
80+
return NO;
81+
}
82+
83+
static void RCTWarnAppLevelStatusBarUnavailable()
84+
{
85+
static dispatch_once_t onceToken;
86+
dispatch_once(&onceToken, ^{
87+
RCTLogWarn(
88+
@"StatusBar has no effect because UIViewControllerBasedStatusBarAppearance is NO in Info.plist. "
89+
@"Apps built with the iOS 27 SDK cannot use the app-level status bar API that this setting uses. "
90+
@"Set UIViewControllerBasedStatusBarAppearance to YES to control the status bar from JavaScript.");
91+
});
92+
}
93+
7194
RCT_EXPORT_MODULE()
7295

7396
+ (BOOL)requiresMainQueueSetup
@@ -147,30 +170,32 @@ - (void)setStyle:(NSString *)style animated:(BOOL)animated
147170
{
148171
dispatch_async(dispatch_get_main_queue(), ^{
149172
UIStatusBarStyle statusBarStyle = [RCTConvert UIStatusBarStyle:style];
150-
if (RCTViewControllerBasedStatusBarAppearance()) {
151-
RCTLogWarn(@"RCTStatusBarManager is a no-op when \
152-
UIViewControllerBasedStatusBarAppearance is YES; set the status bar from your view controller instead");
153-
} else {
173+
[RCTStatusBarAppearance setStyle:statusBarStyle animated:animated];
174+
if (!RCTViewControllerBasedStatusBarAppearance()) {
154175
#pragma clang diagnostic push
155176
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
156177
[RCTSharedApplication() setStatusBarStyle:statusBarStyle animated:animated];
157-
}
158178
#pragma clang diagnostic pop
179+
if (RCTAppLevelStatusBarIsNoOp()) {
180+
RCTWarnAppLevelStatusBarUnavailable();
181+
}
182+
}
159183
});
160184
}
161185

162186
- (void)setHidden:(BOOL)hidden withAnimation:(NSString *)withAnimation
163187
{
164188
dispatch_async(dispatch_get_main_queue(), ^{
165189
UIStatusBarAnimation animation = [RCTConvert UIStatusBarAnimation:withAnimation];
166-
if (RCTViewControllerBasedStatusBarAppearance()) {
167-
RCTLogWarn(@"RCTStatusBarManager is a no-op when \
168-
UIViewControllerBasedStatusBarAppearance is YES; set the status bar from your view controller instead");
169-
} else {
190+
[RCTStatusBarAppearance setHidden:hidden withAnimation:animation];
191+
if (!RCTViewControllerBasedStatusBarAppearance()) {
170192
#pragma clang diagnostic push
171193
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
172194
[RCTSharedApplication() setStatusBarHidden:hidden withAnimation:animation];
173195
#pragma clang diagnostic pop
196+
if (RCTAppLevelStatusBarIsNoOp()) {
197+
RCTWarnAppLevelStatusBarUnavailable();
198+
}
174199
}
175200
});
176201
}

‎packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.mm‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@
88
#import "RCTFabricModalHostViewController.h"
99

1010
#import <React/RCTLog.h>
11+
#import <React/RCTStatusBarAppearance.h>
1112
#import <React/RCTSurfaceTouchHandler.h>
1213

1314
@implementation RCTFabricModalHostViewController {
1415
CGRect _lastViewBounds;
1516
#if !TARGET_OS_TV
1617
RCTSurfaceTouchHandler *_touchHandler;
18+
BOOL _hasCapturedStatusBar;
19+
UIStatusBarStyle _capturedStatusBarStyle;
20+
BOOL _capturedStatusBarHidden;
21+
UIStatusBarStyle _jsStatusBarStyleAtCapture;
22+
BOOL _jsStatusBarHiddenAtCapture;
1723
#endif
1824
}
1925

@@ -47,14 +53,36 @@ - (void)loadView
4753
}
4854

4955
#if !TARGET_OS_TV
56+
- (void)captureStatusBarAppearance
57+
{
58+
UIStatusBarManager *statusBarManager = RCTUIStatusBarManager();
59+
_hasCapturedStatusBar = YES;
60+
_capturedStatusBarStyle = statusBarManager.statusBarStyle;
61+
_capturedStatusBarHidden = statusBarManager.isStatusBarHidden;
62+
_jsStatusBarStyleAtCapture = RCTStatusBarAppearance.style;
63+
_jsStatusBarHiddenAtCapture = RCTStatusBarAppearance.hidden;
64+
}
65+
66+
// Keep the status bar from before the modal until JS changes it.
5067
- (UIStatusBarStyle)preferredStatusBarStyle
5168
{
52-
return [RCTUIStatusBarManager() statusBarStyle];
69+
if (_hasCapturedStatusBar && RCTStatusBarAppearance.style == _jsStatusBarStyleAtCapture) {
70+
return _capturedStatusBarStyle;
71+
}
72+
return RCTStatusBarAppearance.style;
5373
}
5474

5575
- (BOOL)prefersStatusBarHidden
5676
{
57-
return [RCTUIStatusBarManager() isStatusBarHidden];
77+
if (_hasCapturedStatusBar && RCTStatusBarAppearance.hidden == _jsStatusBarHiddenAtCapture) {
78+
return _capturedStatusBarHidden;
79+
}
80+
return RCTStatusBarAppearance.hidden;
81+
}
82+
83+
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
84+
{
85+
return RCTStatusBarAppearance.updateAnimation;
5886
}
5987
#endif
6088

‎packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717

1818
#import "RCTFabricModalHostViewController.h"
1919

20+
#if !TARGET_OS_TV
21+
@interface RCTFabricModalHostViewController ()
22+
- (void)captureStatusBarAppearance;
23+
@end
24+
#endif
25+
2026
using namespace facebook::react;
2127

2228
#if !TARGET_OS_TV
@@ -173,6 +179,9 @@ - (void)ensurePresentedOnlyIfNeeded
173179
[self saveAccessibilityFocusedView];
174180
self.viewController.presentationController.delegate = self;
175181
self.viewController.modalInPresentation = _modalInPresentation;
182+
#if !TARGET_OS_TV
183+
[self.viewController captureStatusBarAppearance];
184+
#endif
176185

177186
_isPresented = YES;
178187
[self presentViewController:self.viewController

‎packages/rn-tester/RNTester/Info.plist‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,6 @@
8181
<string>UIInterfaceOrientationPortraitUpsideDown</string>
8282
</array>
8383
<key>UIViewControllerBasedStatusBarAppearance</key>
84-
<false/>
84+
<true/>
8585
</dict>
8686
</plist>

‎packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
5C60EB1C226440DB0018C04F /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5C60EB1B226440DB0018C04F /* AppDelegate.mm */; };
1818
602160B9478DAFF2BAB56595 /* RCTUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D5D13FB1445C5C19639921CC /* RCTUtilsTests.m */; };
1919
79B29C2E2E607A99007612A5 /* SceneDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 79B29C2D2E607A99007612A5 /* SceneDelegate.mm */; };
20+
7F806D4D4D97AD6403558B35 /* RCTStatusBarAppearanceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 67B0814814F7C07E196BC295 /* RCTStatusBarAppearanceTests.mm */; };
2021
8145AE06241172D900A3F8DA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8145AE05241172D900A3F8DA /* LaunchScreen.storyboard */; };
2122
832F45BB2A8A6E1F0097B4E6 /* SwiftTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */; };
2223
A975CA6C2C05EADF0043F72A /* RCTNetworkTaskTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */; };
@@ -94,6 +95,7 @@
9495
4C706D402EE4AF9BE838CBA9 /* libPods-RNTester.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNTester.a"; sourceTree = BUILT_PRODUCTS_DIR; };
9596
51BC9297B6C3163C14532020 /* Pods-RNTester.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTester.release.xcconfig"; path = "Target Support Files/Pods-RNTester/Pods-RNTester.release.xcconfig"; sourceTree = "<group>"; };
9697
5C60EB1B226440DB0018C04F /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = RNTester/AppDelegate.mm; sourceTree = "<group>"; };
98+
67B0814814F7C07E196BC295 /* RCTStatusBarAppearanceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTStatusBarAppearanceTests.mm; sourceTree = "<group>"; };
9799
79B29C2C2E607A99007612A5 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SceneDelegate.h; path = RNTester/SceneDelegate.h; sourceTree = "<group>"; };
98100
79B29C2D2E607A99007612A5 /* SceneDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = SceneDelegate.mm; path = RNTester/SceneDelegate.mm; sourceTree = "<group>"; };
99101
8145AE05241172D900A3F8DA /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = RNTester/LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -350,6 +352,7 @@
350352
E7DB20D022B2BAA5005AC45F /* RCTURLUtilsTests.m */,
351353
D5D13FB1445C5C19639921CC /* RCTUtilsTests.m */,
352354
E7DB20B322B2BAA4005AC45F /* RNTesterUnitTestsBundle.js */,
355+
67B0814814F7C07E196BC295 /* RCTStatusBarAppearanceTests.mm */,
353356
);
354357
path = RNTesterUnitTests;
355358
sourceTree = "<group>";
@@ -778,6 +781,7 @@
778781
E7DB20E922B2BAA6005AC45F /* RCTComponentPropsTests.m in Sources */,
779782
E7DB20D822B2BAA6005AC45F /* RCTJSONTests.m in Sources */,
780783
602160B9478DAFF2BAB56595 /* RCTUtilsTests.m in Sources */,
784+
7F806D4D4D97AD6403558B35 /* RCTStatusBarAppearanceTests.mm in Sources */,
781785
);
782786
runOnlyForDeploymentPostprocessing = 0;
783787
};

0 commit comments

Comments
 (0)