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 2 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
75 changes: 45 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 true.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "YES", not "true".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

* Explicitly add `UIViewControllerBasedStatusBarAppearance` as `false` in
* info.plist makes this value to be false.
*/
@property(nonatomic, assign) BOOL enableViewControllerBasedStatusBarAppearance;
Comment thread
stuartmorgan-g marked this conversation as resolved.

@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,9 @@ - (instancetype)initWithEngine:(fml::WeakPtr<FlutterEngine>)engine {

if (self) {
_engine = engine;
NSNumber* infoValue = [[NSBundle mainBundle]

@stuartmorgan-g stuartmorgan-g Jun 13, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code (the next line, specifically) will crash if someone puts the wrong key type in their Info.plist. Since that's a developer error that's probably okay, but it might be nice to check the type and log (in debug only) a clear error message if it's wrong.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"];
_enableViewControllerBasedStatusBarAppearance = (infoValue == nil || [infoValue boolValue]);
Comment thread
stuartmorgan-g marked this conversation as resolved.
Outdated
}

return self;
Expand Down Expand Up @@ -158,14 +174,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 +184,36 @@ - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays {
postNotificationName:FlutterViewControllerHideHomeIndicator
object:nil];
}
if (self.enableViewControllerBasedStatusBarAppearance) {
[_engine.get() viewController].flutterPrefersStatusBarHidden = 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].flutterPrefersStatusBarHidden = !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 +239,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 @@ -2081,6 +2081,17 @@ - (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification {
});
}

- (void)setFlutterPrefersStatusBarHidden:(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.
*
* The |UIViewController:prefersStatusBarHidden| of this ViewController is overriden and
* returns `flutterPrefersStatusBarHidden`. This is ignored when
* `UIViewControllerBasedStatusBarAppearance` in info.plist of the app project is `false`.
*/
@property(nonatomic, assign) BOOL flutterPrefersStatusBarHidden;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the structure again here... couldn't you just redeclare prefersStatusBarHidden as read-write, to avoid having two getters that do exactly the same thing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I still kept the local var's name to be _flutterPrefersStatusBarHidden so it is clearer that the getter and setter are overridden.


- (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_