From 80e6d672f32fdc860c73eabcc63763dcab3c6269 Mon Sep 17 00:00:00 2001 From: radex Date: Wed, 4 Mar 2020 14:21:02 -0800 Subject: [PATCH] UIViewController-based status bar management (#25919) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: {emoji:26a0} This is a follow up to https://github.com/facebook/react-native/issues/25425 -- which isn't merged yet… See https://github.com/facebook/react-native/pull/25919/files/2a286257a6553a80a34e2b1f1ad94fc7bae36ea3..125aedbedc234c65c8d1b2133b79e926ad6cf145 for actual diff Currently, StatusBar native module manages the status bar on iOS globally, using `UIApplication.` APIs. This is bad because: - those APIs have been deprecated for 4 years - Apple really, really wants you to have an explicitly defined view controller, and control the status bar there - it [breaks external native components](https://github.com/facebook/react-native/issues/25181#issuecomment-506792819) - it's [not compatible with iPadOS 13 multi window support](https://github.com/facebook/react-native/issues/25181#issuecomment-506690818) for those reasons I we should transition towards view controller-based status bar management. With that, there is a need to introduce a default React Native root view controller, so I added `RCTRootViewController`. Using it is completely opt-in and there is no breaking change here. However I believe this should be a part of the template for new RN iOS apps. Additionally, I added `RCTRootViewControllerProtocol` with hooks needed for RCTStatusBarManager to control the status bar. This means apps that want to have total control over their view controller can still opt in to react native VC-based status bar by conforming their root view controller to this protocol. ## Changelog [iOS] [Added] - Added `RCTRootViewController` and `RCTRootViewControllerProtocol` [iOS] [Fixed] - `UIViewControllerBasedStatusBarAppearance=YES` no longer triggers an error as long as you use `RCTRootViewController` [iOS] [Fixed] - Status bar style is now correctly changed in multi-window iPadOS 13 apps if you use `RCTRootViewController` and set `UIViewControllerBasedStatusBarAppearance=YES` Pull Request resolved: https://github.com/facebook/react-native/pull/25919 Test Plan: - Open RNTester → StatusBar → and check that no features broke Reviewed By: fkgozali Differential Revision: D16957766 Pulled By: hramos fbshipit-source-id: 9ae1384ee20a06933053c4404b8237810f1e7c2c --- RNTester/RNTester/AppDelegate.mm | 8 ++- RNTester/RNTester/Info.plist | 2 - React/Base/RCTRootViewController.h | 52 +++++++++++++++++ React/Base/RCTRootViewController.m | 73 ++++++++++++++++++++++++ React/CoreModules/RCTStatusBarManager.mm | 62 ++++++++++++++------ template/ios/HelloWorld/AppDelegate.m | 4 +- template/ios/HelloWorld/Info.plist | 2 - 7 files changed, 176 insertions(+), 27 deletions(-) create mode 100644 React/Base/RCTRootViewController.h create mode 100644 React/Base/RCTRootViewController.m diff --git a/RNTester/RNTester/AppDelegate.mm b/RNTester/RNTester/AppDelegate.mm index 137ab78d97..22cf56a33b 100644 --- a/RNTester/RNTester/AppDelegate.mm +++ b/RNTester/RNTester/AppDelegate.mm @@ -21,6 +21,7 @@ #import #import #import +#import #import #import @@ -89,12 +90,13 @@ UIView *rootView = [[RCTFabricSurfaceHostingProxyRootView alloc] initWithBridge:_bridge moduleName:@"RNTesterApp" initialProperties:initProps]; #else - UIView *rootView = [[RCTRootView alloc] initWithBridge:_bridge moduleName:@"RNTesterApp" initialProperties:initProps]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:_bridge + moduleName:@"RNTesterApp" + initialProperties:initProps]; #endif self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [UIViewController new]; - rootViewController.view = rootView; + RCTRootViewController *rootViewController = [[RCTRootViewController alloc] initWithRootView:rootView]; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; [self initializeFlipper:application]; diff --git a/RNTester/RNTester/Info.plist b/RNTester/RNTester/Info.plist index 3b0806d0b9..6a14cb7619 100644 --- a/RNTester/RNTester/Info.plist +++ b/RNTester/RNTester/Info.plist @@ -56,8 +56,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - NSPhotoLibraryUsageDescription You need to add NSPhotoLibraryUsageDescription key in Info.plist to enable photo library usage, otherwise it is going to *fail silently*! RN_BUNDLE_PREFIX diff --git a/React/Base/RCTRootViewController.h b/React/Base/RCTRootViewController.h new file mode 100644 index 0000000000..9ed1d1c924 --- /dev/null +++ b/React/Base/RCTRootViewController.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class RCTRootView; + +@protocol RCTRootViewControllerProtocol + +/** + * RCTStatusBarManager calls this to update the status bar style. + * + * Conforming view controllers should use this to update preferred status bar style + */ +- (void)updateStatusBarStyle:(UIStatusBarStyle)style + hidden:(BOOL)hidden + animation:(UIStatusBarAnimation)animation + animated:(BOOL)animate; + +@end + +@interface RCTRootViewController : UIViewController + +/** + * - Designated initializer - + */ +- (instancetype)initWithRootView:(RCTRootView *)rootView NS_DESIGNATED_INITIALIZER; + +/** + * The root view used by the view controller. + */ +@property (nonatomic, strong, readonly) RCTRootView *rootView; + +/** + * See: RCTRootViewControllerProtocol + */ +- (void)updateStatusBarStyle:(UIStatusBarStyle)style + hidden:(BOOL)hidden + animation:(UIStatusBarAnimation)animation + animated:(BOOL)animate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/React/Base/RCTRootViewController.m b/React/Base/RCTRootViewController.m new file mode 100644 index 0000000000..96f61c82ea --- /dev/null +++ b/React/Base/RCTRootViewController.m @@ -0,0 +1,73 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTRootViewController.h" +#import "RCTUtils.h" +#import "RCTRootView.h" + +@implementation RCTRootViewController +{ + UIStatusBarStyle _statusBarStyle; + BOOL _statusBarHidden; + UIStatusBarAnimation _statusBarAnimation; +} + +- (instancetype)initWithRootView:(RCTRootView *)rootView +{ + RCTAssertParam(rootView); + + if (self = [super initWithNibName:nil bundle:nil]) { + _rootView = rootView; + _statusBarStyle = UIStatusBarStyleDefault; + _statusBarHidden = false; + _statusBarAnimation = UIStatusBarAnimationFade; + } + + return self; +} + +RCT_NOT_IMPLEMENTED(- (instancetype)initWithNibName:(NSString *)nn bundle:(NSBundle *)nb) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) + +- (void)loadView +{ + self.view = _rootView; +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return _statusBarStyle; +} + +- (BOOL)prefersStatusBarHidden +{ + return _statusBarHidden; +} + +- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation +{ + return _statusBarAnimation; +} + +- (void)updateStatusBarStyle:(UIStatusBarStyle)style + hidden:(BOOL)hidden + animation:(UIStatusBarAnimation)animation + animated:(BOOL)animate; +{ + _statusBarStyle = style; + _statusBarHidden = hidden; + _statusBarAnimation = animation; + if (animate) { + [UIView animateWithDuration:0.150 animations:^{ + [self setNeedsStatusBarAppearanceUpdate]; + }]; + } else { + [self setNeedsStatusBarAppearanceUpdate]; + } +} + +@end diff --git a/React/CoreModules/RCTStatusBarManager.mm b/React/CoreModules/RCTStatusBarManager.mm index b93805a426..53d550e7fe 100644 --- a/React/CoreModules/RCTStatusBarManager.mm +++ b/React/CoreModules/RCTStatusBarManager.mm @@ -13,6 +13,7 @@ #import #import #import +#import #if !TARGET_OS_TV #import @@ -144,7 +145,27 @@ RCT_EXPORT_MODULE() [self emitEvent:@"statusBarFrameWillChange" forNotification:notification]; } -RCT_EXPORT_METHOD(getHeight : (RCTResponseSenderBlock)callback) +- (UIViewController*) viewControllerForReactTag:(nonnull NSNumber *)reactTag +{ + if (!RCTViewControllerBasedStatusBarAppearance()) { + return nil; + } + + UIView *view = [self.bridge.uiManager viewForReactTag:reactTag]; + UIViewController *viewController = view.window.rootViewController ?: RCTKeyWindow().rootViewController; + + if ([viewController conformsToProtocol:@protocol(RCTRootViewControllerProtocol)]) { + return (UIViewController*) viewController; + } else { + RCTLogError(@"RCTStatusBarManager could not find RCTRootViewController. \ + If UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to YES (recommended for new apps), \ + You need to use RCTRootViewControllerProtocol-conforming view controller as app window's root view controller \ + and must pass a node reference to `surface` argument of StatusBar methods."); + return nil; + } +} + +RCT_EXPORT_METHOD(getHeight:(RCTResponseSenderBlock)callback) { callback(@[ @{ @"height" : @(RCTSharedApplication().statusBarFrame.size.height), @@ -155,19 +176,22 @@ RCT_EXPORT_METHOD(setStyle:(NSString *)style animated:(BOOL)animated reactTag:(double)reactTag) { - UIStatusBarStyle statusBarStyle = [RCTConvert UIStatusBarStyle:style]; - if (RCTViewControllerBasedStatusBarAppearance()) { - RCTLogError(@"RCTStatusBarManager module requires that the \ - UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); - return; - } + // NSNumber *reactTag = options.reactTag() ? @(options.reactTag()) : @-1; + UIStatusBarStyle statusBarStyle = [RCTConvert UIStatusBarStyle:style]; + UIViewController *viewController = [self viewControllerForReactTag:@(reactTag)]; - // TODO (T62270453): Add proper support for UIScenes (this requires view controller based status bar management) + if (viewController) { + [viewController updateStatusBarStyle:statusBarStyle + hidden:viewController.prefersStatusBarHidden + animation:viewController.preferredStatusBarUpdateAnimation + animated:animated]; + } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - [RCTSharedApplication() setStatusBarStyle:statusBarStyle - animated:animated]; + [RCTSharedApplication() setStatusBarStyle:statusBarStyle + animated:animated]; #pragma clang diagnostic pop + } } RCT_EXPORT_METHOD(setHidden:(BOOL)hidden @@ -175,18 +199,20 @@ RCT_EXPORT_METHOD(setHidden:(BOOL)hidden reactTag:(double)reactTag) { UIStatusBarAnimation animation = [RCTConvert UIStatusBarAnimation:withAnimation]; - if (RCTViewControllerBasedStatusBarAppearance()) { - RCTLogError(@"RCTStatusBarManager module requires that the \ - UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); - return; - } + UIViewController *viewController = [self viewControllerForReactTag:@(reactTag)]; - // TODO (T62270453): Add proper support for UIScenes (this requires view controller based status bar management) + if (viewController) { + [viewController updateStatusBarStyle:viewController.preferredStatusBarStyle + hidden:hidden + animation:animation + animated:animation != UIStatusBarAnimationNone]; + } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - [RCTSharedApplication() setStatusBarHidden:hidden - withAnimation:animation]; + [RCTSharedApplication() setStatusBarHidden:hidden + withAnimation:animation]; #pragma clang diagnostic pop + } } RCT_EXPORT_METHOD(setNetworkActivityIndicatorVisible : (BOOL)visible) diff --git a/template/ios/HelloWorld/AppDelegate.m b/template/ios/HelloWorld/AppDelegate.m index c680572d44..79d9e6083b 100644 --- a/template/ios/HelloWorld/AppDelegate.m +++ b/template/ios/HelloWorld/AppDelegate.m @@ -3,6 +3,7 @@ #import #import #import +#import #if DEBUG #import @@ -39,8 +40,7 @@ static void InitializeFlipper(UIApplication *application) { rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [UIViewController new]; - rootViewController.view = rootView; + RCTRootViewController *rootViewController = [[RCTRootViewController alloc] initWithRootView:rootView]; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; return YES; diff --git a/template/ios/HelloWorld/Info.plist b/template/ios/HelloWorld/Info.plist index 20f7dd5114..fba9e97fb8 100644 --- a/template/ios/HelloWorld/Info.plist +++ b/template/ios/HelloWorld/Info.plist @@ -51,7 +51,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance -