Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,34 @@
#endif

#import <RCTAnimatedModuleProvider/RCTAnimatedModuleProvider.h>
#import <React/RCTStatusBarAppearance.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/nativemodule/defaults/DefaultTurboModules.h>

@interface RCTRootViewController : UIViewController
@end

@implementation RCTRootViewController

#if !TARGET_OS_TV
- (UIStatusBarStyle)preferredStatusBarStyle
{
return RCTStatusBarAppearance.style;
}

- (BOOL)prefersStatusBarHidden
{
return RCTStatusBarAppearance.hidden;
}

- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
{
return RCTStatusBarAppearance.updateAnimation;
}
#endif

@end

@implementation RCTDefaultReactNativeFactoryDelegate {
// C++ Native Animated provider, created once on first use (getTurboModule: may be called
// concurrently for different module names).
Expand All @@ -28,7 +53,7 @@ @implementation RCTDefaultReactNativeFactoryDelegate {

- (UIViewController *)createRootViewController
{
return [UIViewController new];
return [RCTRootViewController new];
}

- (void)setRootView:(UIView *)rootView toRootViewController:(UIViewController *)rootViewController
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ NS_ASSUME_NONNULL_BEGIN

/**
* It creates the RootViewController.
* By default, it creates a new instance of a `UIViewController`.
* By default, it creates a view controller that returns the `RCTStatusBarAppearance` values.
* You can override it to provide your own initial ViewController.
*
* @return: an instance of `UIViewController`.
Expand Down
28 changes: 28 additions & 0 deletions packages/react-native/React/Base/RCTStatusBarAppearance.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import <UIKit/UIKit.h>

#if !TARGET_OS_TV
NS_ASSUME_NONNULL_BEGIN

/**
* The status bar state set from JavaScript. Root view controllers return these values from their status bar methods.
*/
@interface RCTStatusBarAppearance : NSObject

@property (class, nonatomic, readonly) UIStatusBarStyle style;
@property (class, nonatomic, readonly) BOOL hidden;
@property (class, nonatomic, readonly) UIStatusBarAnimation updateAnimation;

+ (void)setStyle:(UIStatusBarStyle)style animated:(BOOL)animated;
+ (void)setHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation;

@end

NS_ASSUME_NONNULL_END
#endif
64 changes: 64 additions & 0 deletions packages/react-native/React/Base/RCTStatusBarAppearance.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import "RCTStatusBarAppearance.h"

#if !TARGET_OS_TV
#import "RCTAssert.h"
#import "RCTUtils.h"

static UIStatusBarStyle sStyle = UIStatusBarStyleDefault;
static BOOL sHidden = NO;
static UIStatusBarAnimation sUpdateAnimation = UIStatusBarAnimationNone;

@implementation RCTStatusBarAppearance

+ (UIStatusBarStyle)style
{
return sStyle;
}

+ (BOOL)hidden
{
return sHidden;
}

+ (UIStatusBarAnimation)updateAnimation
{
return sUpdateAnimation;
}

+ (void)setStyle:(UIStatusBarStyle)style animated:(BOOL)animated
{
RCTAssertMainQueue();
sStyle = style;
[self updateStatusBarAnimated:animated];
}

+ (void)setHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation
{
RCTAssertMainQueue();
sHidden = hidden;
sUpdateAnimation = animation;
[self updateStatusBarAnimated:animation != UIStatusBarAnimationNone];
}

+ (void)updateStatusBarAnimated:(BOOL)animated
{
UIViewController *viewController = RCTPresentedViewController();
if (animated) {
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration
animations:^{
[viewController setNeedsStatusBarAppearanceUpdate];
}];
} else {
[viewController setNeedsStatusBarAppearanceUpdate];
}
}

@end
#endif
43 changes: 34 additions & 9 deletions packages/react-native/React/CoreModules/RCTStatusBarManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#import <React/RCTEventDispatcherProtocol.h>
#import <React/RCTInitializing.h>
#import <React/RCTLog.h>
#import <React/RCTStatusBarAppearance.h>
#import <React/RCTUtils.h>

#import <FBReactNativeSpec/FBReactNativeSpec.h>
Expand Down Expand Up @@ -68,6 +69,28 @@ static BOOL RCTViewControllerBasedStatusBarAppearance()
return value;
}

// UIKit makes the app-level setters no-ops only for apps built with the iOS 27 SDK.
static BOOL RCTAppLevelStatusBarIsNoOp()
{
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 270000
if (@available(iOS 27.0, *)) {
return YES;
}
#endif
return NO;
}

static void RCTWarnAppLevelStatusBarUnavailable()
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTLogWarn(
@"StatusBar has no effect because UIViewControllerBasedStatusBarAppearance is NO in Info.plist. "
@"Apps built with the iOS 27 SDK cannot use the app-level status bar API that this setting uses. "
@"Set UIViewControllerBasedStatusBarAppearance to YES to control the status bar from JavaScript.");
});
}

RCT_EXPORT_MODULE()

+ (BOOL)requiresMainQueueSetup
Expand Down Expand Up @@ -147,30 +170,32 @@ - (void)setStyle:(NSString *)style animated:(BOOL)animated
{
dispatch_async(dispatch_get_main_queue(), ^{
UIStatusBarStyle statusBarStyle = [RCTConvert UIStatusBarStyle:style];
if (RCTViewControllerBasedStatusBarAppearance()) {
RCTLogWarn(@"RCTStatusBarManager is a no-op when \
UIViewControllerBasedStatusBarAppearance is YES; set the status bar from your view controller instead");
} else {
[RCTStatusBarAppearance setStyle:statusBarStyle animated:animated];
if (!RCTViewControllerBasedStatusBarAppearance()) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[RCTSharedApplication() setStatusBarStyle:statusBarStyle animated:animated];
}
#pragma clang diagnostic pop
if (RCTAppLevelStatusBarIsNoOp()) {
RCTWarnAppLevelStatusBarUnavailable();
}
}
});
}

- (void)setHidden:(BOOL)hidden withAnimation:(NSString *)withAnimation
{
dispatch_async(dispatch_get_main_queue(), ^{
UIStatusBarAnimation animation = [RCTConvert UIStatusBarAnimation:withAnimation];
if (RCTViewControllerBasedStatusBarAppearance()) {
RCTLogWarn(@"RCTStatusBarManager is a no-op when \
UIViewControllerBasedStatusBarAppearance is YES; set the status bar from your view controller instead");
} else {
[RCTStatusBarAppearance setHidden:hidden withAnimation:animation];
if (!RCTViewControllerBasedStatusBarAppearance()) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[RCTSharedApplication() setStatusBarHidden:hidden withAnimation:animation];
#pragma clang diagnostic pop
if (RCTAppLevelStatusBarIsNoOp()) {
RCTWarnAppLevelStatusBarUnavailable();
}
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@
#import "RCTFabricModalHostViewController.h"

#import <React/RCTLog.h>
#import <React/RCTStatusBarAppearance.h>
#import <React/RCTSurfaceTouchHandler.h>

@implementation RCTFabricModalHostViewController {
CGRect _lastViewBounds;
#if !TARGET_OS_TV
RCTSurfaceTouchHandler *_touchHandler;
BOOL _hasCapturedStatusBar;
UIStatusBarStyle _capturedStatusBarStyle;
BOOL _capturedStatusBarHidden;
UIStatusBarStyle _jsStatusBarStyleAtCapture;
BOOL _jsStatusBarHiddenAtCapture;
#endif
}

Expand Down Expand Up @@ -47,14 +53,36 @@ - (void)loadView
}

#if !TARGET_OS_TV
- (void)captureStatusBarAppearance
{
UIStatusBarManager *statusBarManager = RCTUIStatusBarManager();
_hasCapturedStatusBar = YES;
_capturedStatusBarStyle = statusBarManager.statusBarStyle;
_capturedStatusBarHidden = statusBarManager.isStatusBarHidden;
_jsStatusBarStyleAtCapture = RCTStatusBarAppearance.style;
_jsStatusBarHiddenAtCapture = RCTStatusBarAppearance.hidden;
}

// Keep the status bar from before the modal until JS changes it.
- (UIStatusBarStyle)preferredStatusBarStyle
{
return [RCTUIStatusBarManager() statusBarStyle];
if (_hasCapturedStatusBar && RCTStatusBarAppearance.style == _jsStatusBarStyleAtCapture) {
return _capturedStatusBarStyle;
}
return RCTStatusBarAppearance.style;
}

- (BOOL)prefersStatusBarHidden
{
return [RCTUIStatusBarManager() isStatusBarHidden];
if (_hasCapturedStatusBar && RCTStatusBarAppearance.hidden == _jsStatusBarHiddenAtCapture) {
return _capturedStatusBarHidden;
}
return RCTStatusBarAppearance.hidden;
}

- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
{
return RCTStatusBarAppearance.updateAnimation;
}
#endif

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@

#import "RCTFabricModalHostViewController.h"

#if !TARGET_OS_TV
@interface RCTFabricModalHostViewController ()
- (void)captureStatusBarAppearance;
@end
#endif

using namespace facebook::react;

#if !TARGET_OS_TV
Expand Down Expand Up @@ -173,6 +179,9 @@ - (void)ensurePresentedOnlyIfNeeded
[self saveAccessibilityFocusedView];
self.viewController.presentationController.delegate = self;
self.viewController.modalInPresentation = _modalInPresentation;
#if !TARGET_OS_TV
[self.viewController captureStatusBarAppearance];
#endif

_isPresented = YES;
[self presentViewController:self.viewController
Expand Down
2 changes: 1 addition & 1 deletion packages/rn-tester/RNTester/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<true/>
</dict>
</plist>
4 changes: 4 additions & 0 deletions packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
5C60EB1C226440DB0018C04F /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5C60EB1B226440DB0018C04F /* AppDelegate.mm */; };
602160B9478DAFF2BAB56595 /* RCTUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D5D13FB1445C5C19639921CC /* RCTUtilsTests.m */; };
79B29C2E2E607A99007612A5 /* SceneDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 79B29C2D2E607A99007612A5 /* SceneDelegate.mm */; };
7F806D4D4D97AD6403558B35 /* RCTStatusBarAppearanceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 67B0814814F7C07E196BC295 /* RCTStatusBarAppearanceTests.mm */; };
8145AE06241172D900A3F8DA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8145AE05241172D900A3F8DA /* LaunchScreen.storyboard */; };
832F45BB2A8A6E1F0097B4E6 /* SwiftTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */; };
A975CA6C2C05EADF0043F72A /* RCTNetworkTaskTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */; };
Expand Down Expand Up @@ -94,6 +95,7 @@
4C706D402EE4AF9BE838CBA9 /* libPods-RNTester.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNTester.a"; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
5C60EB1B226440DB0018C04F /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = RNTester/AppDelegate.mm; sourceTree = "<group>"; };
67B0814814F7C07E196BC295 /* RCTStatusBarAppearanceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTStatusBarAppearanceTests.mm; sourceTree = "<group>"; };
79B29C2C2E607A99007612A5 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SceneDelegate.h; path = RNTester/SceneDelegate.h; sourceTree = "<group>"; };
79B29C2D2E607A99007612A5 /* SceneDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = SceneDelegate.mm; path = RNTester/SceneDelegate.mm; sourceTree = "<group>"; };
8145AE05241172D900A3F8DA /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = RNTester/LaunchScreen.storyboard; sourceTree = "<group>"; };
Expand Down Expand Up @@ -350,6 +352,7 @@
E7DB20D022B2BAA5005AC45F /* RCTURLUtilsTests.m */,
D5D13FB1445C5C19639921CC /* RCTUtilsTests.m */,
E7DB20B322B2BAA4005AC45F /* RNTesterUnitTestsBundle.js */,
67B0814814F7C07E196BC295 /* RCTStatusBarAppearanceTests.mm */,
);
path = RNTesterUnitTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -778,6 +781,7 @@
E7DB20E922B2BAA6005AC45F /* RCTComponentPropsTests.m in Sources */,
E7DB20D822B2BAA6005AC45F /* RCTJSONTests.m in Sources */,
602160B9478DAFF2BAB56595 /* RCTUtilsTests.m in Sources */,
7F806D4D4D97AD6403558B35 /* RCTStatusBarAppearanceTests.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Loading
Loading