Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
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
82 changes: 52 additions & 30 deletions shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<FlutterEngine> _engine;
// Used to detect whether this device has live text input ability or not.
Expand All @@ -47,6 +60,16 @@ - (instancetype)initWithEngine:(fml::WeakPtr<FlutterEngine>)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;
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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<fml::WeakPtrFactory<FlutterEngine>> _weakFactory =
std::make_unique<fml::WeakPtrFactory<FlutterEngine>>(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<fml::WeakPtrFactory<FlutterEngine>> _weakFactory =
std::make_unique<fml::WeakPtrFactory<FlutterEngine>>(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
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ @implementation FlutterViewController {
}

@synthesize displayingFlutterUI = _displayingFlutterUI;
@synthesize prefersStatusBarHidden = _flutterPrefersStatusBarHidden;

#pragma mark - Manage and override all designated initializers

Expand Down Expand Up @@ -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<flutter::FlutterPlatformViewsController>&)platformViewsController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FlutterViewController>)getWeakPtr;
- (std::shared_ptr<flutter::FlutterPlatformViewsController>&)platformViewsController;
- (FlutterRestorationPlugin*)restorationPlugin;
Expand All @@ -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_