diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index 3e81a39f2c779..6d79dcee5179b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -35,6 +35,19 @@ using namespace flutter; +@interface FlutterPlatformPlugin () + +/** + * @brief Whether the status bar appearance is based on the style preferred for this ViewController. + * + * The default value is YES. + * Explicitly add `UIViewControllerBasedStatusBarAppearance` as `false` in + * info.plist makes this value to be false. + */ +@property(nonatomic, assign) BOOL enableViewControllerBasedStatusBarAppearance; + +@end + @implementation FlutterPlatformPlugin { fml::WeakPtr _engine; // Used to detect whether this device has live text input ability or not. @@ -47,6 +60,16 @@ - (instancetype)initWithEngine:(fml::WeakPtr)engine { if (self) { _engine = engine; + NSObject* infoValue = [[NSBundle mainBundle] + objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"]; +#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG + if (infoValue != nil && ![infoValue isKindOfClass:[NSNumber class]]) { + FML_LOG(ERROR) << "The value of UIViewControllerBasedStatusBarAppearance in info.plist must " + "be a Boolean type."; + } +#endif + _enableViewControllerBasedStatusBarAppearance = + (infoValue == nil || [(NSNumber*)infoValue boolValue]); } return self; @@ -158,14 +181,7 @@ - (void)setSystemChromeApplicationSwitcherDescription:(NSDictionary*)object { } - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays { - // Checks if the top status bar should be visible. This platform ignores all - // other overlays - - // We opt out of view controller based status bar visibility since we want - // to be able to modify this on the fly. The key used is - // UIViewControllerBasedStatusBarAppearance - [UIApplication sharedApplication].statusBarHidden = - ![overlays containsObject:@"SystemUiOverlay.top"]; + BOOL statusBarShouldBeHidden = ![overlays containsObject:@"SystemUiOverlay.top"]; if ([overlays containsObject:@"SystemUiOverlay.bottom"]) { [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerShowHomeIndicator @@ -175,26 +191,36 @@ - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays { postNotificationName:FlutterViewControllerHideHomeIndicator object:nil]; } + if (self.enableViewControllerBasedStatusBarAppearance) { + [_engine.get() viewController].prefersStatusBarHidden = statusBarShouldBeHidden; + } else { + // Checks if the top status bar should be visible. This platform ignores all + // other overlays + + // We opt out of view controller based status bar visibility since we want + // to be able to modify this on the fly. The key used is + // UIViewControllerBasedStatusBarAppearance. + [UIApplication sharedApplication].statusBarHidden = statusBarShouldBeHidden; + } } - (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode { - // Checks if the top status bar should be visible, reflected by edge to edge setting. This - // platform ignores all other system ui modes. - - // We opt out of view controller based status bar visibility since we want - // to be able to modify this on the fly. The key used is - // UIViewControllerBasedStatusBarAppearance - [UIApplication sharedApplication].statusBarHidden = - ![mode isEqualToString:@"SystemUiMode.edgeToEdge"]; - if ([mode isEqualToString:@"SystemUiMode.edgeToEdge"]) { - [[NSNotificationCenter defaultCenter] - postNotificationName:FlutterViewControllerShowHomeIndicator - object:nil]; + BOOL edgeToEdge = [mode isEqualToString:@"SystemUiMode.edgeToEdge"]; + if (self.enableViewControllerBasedStatusBarAppearance) { + [_engine.get() viewController].prefersStatusBarHidden = !edgeToEdge; } else { - [[NSNotificationCenter defaultCenter] - postNotificationName:FlutterViewControllerHideHomeIndicator - object:nil]; + // Checks if the top status bar should be visible, reflected by edge to edge setting. This + // platform ignores all other system ui modes. + + // We opt out of view controller based status bar visibility since we want + // to be able to modify this on the fly. The key used is + // UIViewControllerBasedStatusBarAppearance. + [UIApplication sharedApplication].statusBarHidden = !edgeToEdge; } + [[NSNotificationCenter defaultCenter] + postNotificationName:edgeToEdge ? FlutterViewControllerShowHomeIndicator + : FlutterViewControllerHideHomeIndicator + object:nil]; } - (void)restoreSystemChromeSystemUIOverlays { @@ -220,19 +246,15 @@ - (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message { return; } - NSNumber* infoValue = [[NSBundle mainBundle] - objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"]; - Boolean delegateToViewController = (infoValue == nil || [infoValue boolValue]); - - if (delegateToViewController) { - // This notification is respected by the iOS embedder + if (self.enableViewControllerBasedStatusBarAppearance) { + // This notification is respected by the iOS embedder. [[NSNotificationCenter defaultCenter] postNotificationName:@(kOverlayStyleUpdateNotificationName) object:nil userInfo:@{@(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle)}]; } else { // Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9 - // in favor of delegating to the view controller + // in favor of delegating to the view controller. [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle]; } } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm index fec7983d7a8c2..b07261352abaf 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -8,6 +8,7 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" @@ -138,4 +139,93 @@ - (void)testWhetherDeviceHasLiveTextInputInvokeCorrectly { [self waitForExpectationsWithTimeout:1 handler:nil]; } +- (void)testViewControllerBasedStatusBarHiddenUpdate { + id bundleMock = OCMPartialMock([NSBundle mainBundle]); + OCMStub([bundleMock objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"]) + .andReturn(@YES); + { + // Enabling system UI overlays to update status bar. + FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease]; + [engine runWithEntrypoint:nil]; + FlutterViewController* flutterViewController = + [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + std::unique_ptr> _weakFactory = + std::make_unique>(engine); + XCTAssertFalse(flutterViewController.prefersStatusBarHidden); + + // Update to hidden. + FlutterPlatformPlugin* plugin = [engine platformPlugin]; + + XCTestExpectation* enableSystemUIOverlaysCalled = + [self expectationWithDescription:@"setEnabledSystemUIOverlays"]; + FlutterResult resultSet = ^(id result) { + [enableSystemUIOverlaysCalled fulfill]; + }; + FlutterMethodCall* methodCallSet = + [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setEnabledSystemUIOverlays" + arguments:@[ @"SystemUiOverlay.bottom" ]]; + [plugin handleMethodCall:methodCallSet result:resultSet]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(flutterViewController.prefersStatusBarHidden); + + // Update to shown. + XCTestExpectation* enableSystemUIOverlaysCalled2 = + [self expectationWithDescription:@"setEnabledSystemUIOverlays"]; + FlutterResult resultSet2 = ^(id result) { + [enableSystemUIOverlaysCalled2 fulfill]; + }; + FlutterMethodCall* methodCallSet2 = + [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setEnabledSystemUIOverlays" + arguments:@[ @"SystemUiOverlay.top" ]]; + [plugin handleMethodCall:methodCallSet2 result:resultSet2]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertFalse(flutterViewController.prefersStatusBarHidden); + + [flutterViewController deregisterNotifications]; + [flutterViewController release]; + } + { + // Enable system UI mode to update status bar. + FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease]; + [engine runWithEntrypoint:nil]; + FlutterViewController* flutterViewController = + [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + std::unique_ptr> _weakFactory = + std::make_unique>(engine); + XCTAssertFalse(flutterViewController.prefersStatusBarHidden); + + // Update to hidden. + FlutterPlatformPlugin* plugin = [engine platformPlugin]; + + XCTestExpectation* enableSystemUIModeCalled = + [self expectationWithDescription:@"setEnabledSystemUIMode"]; + FlutterResult resultSet = ^(id result) { + [enableSystemUIModeCalled fulfill]; + }; + FlutterMethodCall* methodCallSet = + [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setEnabledSystemUIMode" + arguments:@"SystemUiMode.immersive"]; + [plugin handleMethodCall:methodCallSet result:resultSet]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertTrue(flutterViewController.prefersStatusBarHidden); + + // Update to shown. + XCTestExpectation* enableSystemUIModeCalled2 = + [self expectationWithDescription:@"setEnabledSystemUIMode"]; + FlutterResult resultSet2 = ^(id result) { + [enableSystemUIModeCalled2 fulfill]; + }; + FlutterMethodCall* methodCallSet2 = + [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setEnabledSystemUIMode" + arguments:@"SystemUiMode.edgeToEdge"]; + [plugin handleMethodCall:methodCallSet2 result:resultSet2]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + XCTAssertFalse(flutterViewController.prefersStatusBarHidden); + + [flutterViewController deregisterNotifications]; + [flutterViewController release]; + } + [bundleMock stopMocking]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 1831b4d2f546b..9aaee1299fe1d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -146,6 +146,7 @@ @implementation FlutterViewController { } @synthesize displayingFlutterUI = _displayingFlutterUI; +@synthesize prefersStatusBarHidden = _flutterPrefersStatusBarHidden; #pragma mark - Manage and override all designated initializers @@ -2081,6 +2082,17 @@ - (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification { }); } +- (void)setPrefersStatusBarHidden:(BOOL)hidden { + if (hidden != _flutterPrefersStatusBarHidden) { + _flutterPrefersStatusBarHidden = hidden; + [self setNeedsStatusBarAppearanceUpdate]; + } +} + +- (BOOL)prefersStatusBarHidden { + return _flutterPrefersStatusBarHidden; +} + #pragma mark - Platform views - (std::shared_ptr&)platformViewsController { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index 664b4c282b9f8..b7af6340c0f34 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h @@ -39,6 +39,16 @@ typedef NS_ENUM(NSInteger, FlutterKeyboardMode) { @property(nonatomic, readonly) BOOL isPresentingViewController; @property(nonatomic, readonly) BOOL isVoiceOverRunning; @property(nonatomic, retain) FlutterKeyboardManager* keyboardManager; + +/** + * @brief Whether the status bar is preferred hidden. + * + * This overrides the |UIViewController:prefersStatusBarHidden|. + * This is ignored when `UIViewControllerBasedStatusBarAppearance` in info.plist + * of the app project is `false`. + */ +@property(nonatomic, assign, readwrite) BOOL prefersStatusBarHidden; + - (fml::WeakPtr)getWeakPtr; - (std::shared_ptr&)platformViewsController; - (FlutterRestorationPlugin*)restorationPlugin; @@ -53,6 +63,7 @@ typedef NS_ENUM(NSInteger, FlutterKeyboardMode) { - (void)addInternalPlugins; - (void)deregisterNotifications; - (int32_t)accessibilityFlags; + @end #endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVIEWCONTROLLER_INTERNAL_H_