Skip to content

Commit bd08daf

Browse files
alanjhughesmeta-codesync[bot]
authored andcommitted
Fix StatusBar style and visibility on iOS 27 (#58926)
Summary: Fixes #58758. In apps built with the iOS 27 SDK, `StatusBar.setBarStyle` and `StatusBar.setHidden` do nothing. `RCTStatusBarManager` uses the `UIApplication` status bar setters, and UIKit turns them into no-ops for apps linked against the iOS 27 SDK. Apps built with the iOS 26 SDK are not affected. A plain UIKit app built with the iOS 27 SDK shows that with `UIViewControllerBasedStatusBarAppearance` set to `NO`, nothing can change the status bar on iOS 27. With `YES`, the view controller methods still work. This change moves `StatusBar` to the view controller model. The module stores the requested style, hidden state, and animation in a new `RCTStatusBarAppearance` class and calls `setNeedsStatusBarAppearanceUpdate` on the presented view controller. The default root view controller from `RCTDefaultReactNativeFactoryDelegate` and the Fabric modal controller return those values. The modal keeps the status bar that was on screen when it opened until JS changes it, so a status bar set natively (ie by react-native-screens) is not lost. With `NO`, the module still calls the `UIApplication` setters, so apps built with older SDKs keep working. When React Native is built with the iOS 27 SDK and runs on iOS 27, it also logs one warning that tells the developer to set the key to `YES`. Apps that override `createRootViewController` need their view controller to return the `RCTStatusBarAppearance` values for `StatusBar` to work on iOS 27. A similar change, #25919, landed in March 2020 and was reverted the next day ## Changelog: [IOS] [FIXED] - `StatusBar` style and visibility work on iOS 27 for apps built with the iOS 27 SDK when `UIViewControllerBasedStatusBarAppearance` is `YES`. Custom root view controllers must return the `RCTStatusBarAppearance` values. Pull Request resolved: #58926 Test Plan: 12 new XCTest cases in `RCTStatusBarAppearanceTests`. The OSS XCTest file has no internal BUCK owner and was not run locally. Validation for the review update: - Applied the explicit legacy-API guard to both `setStyle:animated:` and `setHidden:withAnimation:`. The appearance-state update remains unconditional; UIKit setters and warnings remain limited to apps using app-level status bar appearance. - Meta-managed `clang-format` and `arc lint` passed for `RCTStatusBarManager.mm`. - Built `ReactApple`, `CoreModulesApple`, `RCTFabricApple` and `RCTAppDelegateApple` for `iphonesimulator-arm64` with annotation checks enabled. - No public headers changed, so API snapshots are unchanged. - Diff-time CI will rerun for the updated code. Reviewed By: javache Differential Revision: D127555263 Pulled By: cipolleschi fbshipit-source-id: 9a6d21236987e309917e620387b34e88e7da24c7
1 parent d576b40 commit bd08daf

15 files changed

Lines changed: 418 additions & 15 deletions

File tree

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,32 @@
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+
- (UIStatusBarStyle)preferredStatusBarStyle
27+
{
28+
return RCTStatusBarAppearance.style;
29+
}
30+
31+
- (BOOL)prefersStatusBarHidden
32+
{
33+
return RCTStatusBarAppearance.hidden;
34+
}
35+
36+
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
37+
{
38+
return RCTStatusBarAppearance.updateAnimation;
39+
}
40+
41+
@end
42+
2043
@implementation RCTDefaultReactNativeFactoryDelegate {
2144
// C++ Native Animated provider, created once on first use (getTurboModule: may be called
2245
// concurrently for different module names).
@@ -35,7 +58,7 @@ - (NSURL *_Nullable)sourceURLForBridge:(nonnull RCTBridge *)bridge
3558

3659
- (UIViewController *)createRootViewController
3760
{
38-
return [UIViewController new];
61+
return [RCTRootViewController new];
3962
}
4063

4164
- (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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
NS_ASSUME_NONNULL_BEGIN
11+
12+
/**
13+
* The status bar state set from JavaScript. Root view controllers return these values from their status bar methods.
14+
*/
15+
@interface RCTStatusBarAppearance : NSObject
16+
17+
@property (class, nonatomic, readonly) UIStatusBarStyle style;
18+
@property (class, nonatomic, readonly) BOOL hidden;
19+
@property (class, nonatomic, readonly) UIStatusBarAnimation updateAnimation;
20+
21+
+ (void)setStyle:(UIStatusBarStyle)style animated:(BOOL)animated;
22+
+ (void)setHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation;
23+
24+
@end
25+
26+
NS_ASSUME_NONNULL_END
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
#import "RCTAssert.h"
11+
#import "RCTUtils.h"
12+
13+
static UIStatusBarStyle sStyle = UIStatusBarStyleDefault;
14+
static BOOL sHidden = NO;
15+
static UIStatusBarAnimation sUpdateAnimation = UIStatusBarAnimationNone;
16+
17+
@implementation RCTStatusBarAppearance
18+
19+
+ (UIStatusBarStyle)style
20+
{
21+
return sStyle;
22+
}
23+
24+
+ (BOOL)hidden
25+
{
26+
return sHidden;
27+
}
28+
29+
+ (UIStatusBarAnimation)updateAnimation
30+
{
31+
return sUpdateAnimation;
32+
}
33+
34+
+ (void)setStyle:(UIStatusBarStyle)style animated:(BOOL)animated
35+
{
36+
RCTAssertMainQueue();
37+
sStyle = style;
38+
[self updateStatusBarAnimated:animated];
39+
}
40+
41+
+ (void)setHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation
42+
{
43+
RCTAssertMainQueue();
44+
sHidden = hidden;
45+
sUpdateAnimation = animation;
46+
[self updateStatusBarAnimated:animation != UIStatusBarAnimationNone];
47+
}
48+
49+
+ (void)updateStatusBarAnimated:(BOOL)animated
50+
{
51+
UIViewController *viewController = RCTPresentedViewController();
52+
if (animated) {
53+
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration
54+
animations:^{
55+
[viewController setNeedsStatusBarAppearanceUpdate];
56+
}];
57+
} else {
58+
[viewController setNeedsStatusBarAppearanceUpdate];
59+
}
60+
}
61+
62+
@end

‎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,13 +8,19 @@
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;
1718
#endif
19+
BOOL _hasCapturedStatusBar;
20+
UIStatusBarStyle _capturedStatusBarStyle;
21+
BOOL _capturedStatusBarHidden;
22+
UIStatusBarStyle _jsStatusBarStyleAtCapture;
23+
BOOL _jsStatusBarHiddenAtCapture;
1824
}
1925

2026
- (instancetype)init
@@ -46,15 +52,37 @@ - (void)loadView
4652
#endif
4753
}
4854

55+
- (void)captureStatusBarAppearance
56+
{
57+
UIStatusBarManager *statusBarManager = RCTUIStatusBarManager();
58+
_hasCapturedStatusBar = YES;
59+
_capturedStatusBarStyle = statusBarManager.statusBarStyle;
60+
_capturedStatusBarHidden = statusBarManager.isStatusBarHidden;
61+
_jsStatusBarStyleAtCapture = RCTStatusBarAppearance.style;
62+
_jsStatusBarHiddenAtCapture = RCTStatusBarAppearance.hidden;
63+
}
64+
4965
#if !TARGET_OS_TV
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717

1818
#import "RCTFabricModalHostViewController.h"
1919

20+
@interface RCTFabricModalHostViewController ()
21+
- (void)captureStatusBarAppearance;
22+
@end
23+
2024
using namespace facebook::react;
2125

2226
#if !TARGET_OS_TV
@@ -173,6 +177,7 @@ - (void)ensurePresentedOnlyIfNeeded
173177
[self saveAccessibilityFocusedView];
174178
self.viewController.presentationController.delegate = self;
175179
self.viewController.modalInPresentation = _modalInPresentation;
180+
[self.viewController captureStatusBarAppearance];
176181

177182
_isPresented = YES;
178183
[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)