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 1 commit
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 @@ -23,6 +23,8 @@ extern const char* const kOrientationUpdateNotificationName;
extern const char* const kOrientationUpdateNotificationKey;
extern const char* const kOverlayStyleUpdateNotificationName;
extern const char* const kOverlayStyleUpdateNotificationKey;
extern const char* const kStatusBarHiddenUpdateNotificationName;
extern const char* const kStatusBarHiddenUpdateNotificationKey;

} // namespace flutter

Expand Down
76 changes: 49 additions & 27 deletions shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,21 @@
"io.flutter.plugin.platform.SystemChromeOverlayNotificationName";
const char* const kOverlayStyleUpdateNotificationKey =
"io.flutter.plugin.platform.SystemChromeOverlayNotificationKey";
const char* const kStatusBarHiddenUpdateNotificationName =
"io.flutter.plugin.platform.StatusBarHiddenNotificationName";
const char* const kStatusBarHiddenUpdateNotificationKey =
"io.flutter.plugin.platform.StatusBarHiddenNotificationKey";

} // namespace flutter

using namespace flutter;

@interface FlutterPlatformPlugin ()

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

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

@stuartmorgan-g stuartmorgan-g Jun 13, 2023

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
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]);
}

return self;
Expand Down Expand Up @@ -158,14 +171,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 +181,46 @@ - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays {
postNotificationName:FlutterViewControllerHideHomeIndicator
object:nil];
}
if (self.enableViewControllerBasedStatusBarAppearance) {
// This notification is respected by the iOS embedder
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: comments are missing periods.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we communicating within the embedder via notifications, rather than an explicit delegate or callback relationship? My previous experience with notification for communication within a project is that it's significant headache for understanding how components interact.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just copy pasted the old code and didn't really dig into the reason behind it. Most of the notification code are 5+ year old. And looking at the https://github.com/flutter/engine/blob/8c3eeb572f91b86c4fc6a5b90e85d6d008de3dc7/shell/platform/darwin/ios/BUILD.gn at the time, both FlutterPlatformPlugin and FlutterViewController are under the same build target. So I think it was probably just overlooked.

I will remove this notification and also create an issue to move away from the notifications.
I'm not sure if there are apps out there using these notifications directly. We should probably keep these notification for now and slowly deprecate them.

[[NSNotificationCenter defaultCenter]
postNotificationName:@(kStatusBarHiddenUpdateNotificationName)
object:nil
userInfo:@{
@(kStatusBarHiddenUpdateNotificationKey) : @(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"]) {
BOOL edgeToEdge = [mode isEqualToString:@"SystemUiMode.edgeToEdge"];
if (self.enableViewControllerBasedStatusBarAppearance) {
// This notification is respected by the iOS embedder
[[NSNotificationCenter defaultCenter]
postNotificationName:FlutterViewControllerShowHomeIndicator
object:nil];
postNotificationName:@(kStatusBarHiddenUpdateNotificationName)
object:nil
userInfo:@{@(kStatusBarHiddenUpdateNotificationKey) : @(!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,11 +246,7 @@ - (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message {
return;
}

NSNumber* infoValue = [[NSBundle mainBundle]
objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"];
Boolean delegateToViewController = (infoValue == nil || [infoValue boolValue]);

if (delegateToViewController) {
if (self.enableViewControllerBasedStatusBarAppearance) {
// This notification is respected by the iOS embedder
[[NSNotificationCenter defaultCenter]
postNotificationName:@(kOverlayStyleUpdateNotificationName)
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 @@ -103,6 +103,13 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat
@property(nonatomic, retain)
UIRotationGestureRecognizer* rotationGestureRecognizer API_AVAILABLE(ios(13.4));

// Whether the status bar is prefered hidden.
//
// The |UIViewController:prefersStatusBarHidden| of this ViewController is overriden and returns
// `flutterPreferesStatusBarHidden`. Only works when `UIViewControllerBasedStatusBarAppearance` in
// info.plist of the app project is `true`.
@property(nonatomic, assign) BOOL flutterPreferesStatusBarHidden;
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo: prefers


/**
* Creates and registers plugins used by this view controller.
*/
Expand Down Expand Up @@ -299,6 +306,10 @@ - (void)setupNotificationCenterObservers {
selector:@selector(onPreferredStatusBarStyleUpdated:)
name:@(flutter::kOverlayStyleUpdateNotificationName)
object:nil];
[center addObserver:self
selector:@selector(onPreferredStatusBarHiddenUpdated:)
name:@(flutter::kStatusBarHiddenUpdateNotificationName)
object:nil];

[center addObserver:self
selector:@selector(applicationBecameActive:)
Expand Down Expand Up @@ -2081,6 +2092,31 @@ - (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification {
});
}

- (void)onPreferredStatusBarHiddenUpdated:(NSNotification*)notification {
FML_DCHECK(
[notification.name isEqualToString:@(flutter::kStatusBarHiddenUpdateNotificationName)]);
// Notifications may not be on the iOS UI thread
fml::TaskRunner::RunNowOrPostTask([_engine.get() platformTaskRunner], [&]() {
NSDictionary* info = notification.userInfo;

NSNumber* update = info[@(flutter::kStatusBarHiddenUpdateNotificationKey)];

if (update == nil) {
return;
}

BOOL hidden = [update boolValue];
if (hidden != self.flutterPreferesStatusBarHidden) {
self.flutterPreferesStatusBarHidden = hidden;
[self setNeedsStatusBarAppearanceUpdate];
}
});
}

- (BOOL)prefersStatusBarHidden {
return self.flutterPreferesStatusBarHidden;
}

#pragma mark - Platform views

- (std::shared_ptr<flutter::FlutterPlatformViewsController>&)platformViewsController {
Expand Down