Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Closed
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
6 changes: 4 additions & 2 deletions common/config.gni
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ declare_args() {

# Whether to include backtrace support.
enable_backtrace = true

# Whether to include --fapplication-extension when build iOS framework.
darwin_extension_safe = false
}

# feature_defines_list ---------------------------------------------------------
Expand Down Expand Up @@ -61,10 +64,9 @@ if (is_ios || is_mac) {
"-Werror=overriding-method-mismatch",
"-Werror=undeclared-selector",
]
if (is_mac) {
if (darwin_extension_safe || is_mac) {
flutter_cflags_objc += [ "-fapplication-extension" ]
Copy link
Contributor Author

@cyanglaz cyanglaz Apr 6, 2023

Choose a reason for hiding this comment

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

macOS doesn't fail when building with this flag, but macOS does contain NSApplication code. I'm confused here.

@jmagman @cbracken

Copy link
Member

Choose a reason for hiding this comment

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

I didn't get this working either, might take more investigation to see how what flags Xcode is passing through when it shows the warning.

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 tested the locally built mac engine.
Without with this flag, I got the warning when linking a share extension to the framework.
With this flag, there is no warning.
So it seemed like it is just working (which clearly is not since the mac os code does contain NSApplication.)

Will investigate more.
(I should have marked this PR as draft, it is not intended for reviewing yet before this macOS mystery is resolved)

Copy link
Contributor Author

@cyanglaz cyanglaz Apr 7, 2023

Choose a reason for hiding this comment

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

I looked into the headers, on iOS:

@property(class, nonatomic, readonly) UIApplication *sharedApplication NS_EXTENSION_UNAVAILABLE_IOS("Use view controller based solutions where appropriate instead.");
The shared application is marked as extension unavailable.

However, on macOS it is not.
@property (class, readonly, strong) __kindof NSApplication *sharedApplication;

In the document: https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionOverview.html#//apple_ref/doc/uid/TP40014214-CH2-SW6 only linked to the UIApplication.sharedApplication, didn't mention NSApplication.

I create a sample mac framework having Require Only App-Extension-safe API turned on and having NSApplication.SharedApplication. It successfully compiled and a share extension can link to it without warning.

So I believe NSApplication.sharedApplication is compatible with extensions? So on mac, as long as the -fapplication-extension is in the build flags and there is no compile error, it will work.

Copy link
Member

Choose a reason for hiding this comment

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

So I believe NSApplication.sharedApplication is compatible with extensions? So on mac, as long as the -fapplication-extension is in the build flags and there is no compile error, it will work.

!!!
From my reading of the docs when they said sharedApplication without specifying an object I thought they meant either UIApplication or NSApplication.

Access a sharedApplication object, and so cannot use any of the methods on that object

If we don't need to do anything other than documentation for macOS that would be amazing!

Copy link
Member

Choose a reason for hiding this comment

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

And also you could pull that very simple macOS flag-only work out of this so it would start working in the next stable.

Copy link
Member

Choose a reason for hiding this comment

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

By "next stable" I just mean it can be available now-ish without needing to wait for all the iOS work to be correct.

}

flutter_cflags_objcc = flutter_cflags_objc
flutter_cflags_objc_arc = flutter_cflags_objc + [ "-fobjc-arc" ]
flutter_cflags_objcc_arc = flutter_cflags_objc_arc
Expand Down
31 changes: 28 additions & 3 deletions shell/platform/darwin/ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import("//flutter/shell/platform/darwin/common/framework_shared.gni")
import("//flutter/testing/testing.gni")

_flutter_framework_dir = "$root_out_dir/Flutter.framework"
_flutter_xcframework_dir = "$root_out_dir/Flutter.xcframework"

shell_gpu_configuration("ios_gpu_configuration") {
enable_software = true
Expand Down Expand Up @@ -44,6 +45,9 @@ source_set("flutter_framework_source_arc") {
cflags_objcc = flutter_cflags_objcc_arc

defines = [ "FLUTTER_FRAMEWORK=1" ]
if (darwin_extension_safe) {
defines += [ "APPLICATION_EXTENSION_API_ONLY=1" ]
}
allow_circular_includes_from = [ ":flutter_framework_source" ]
deps = [
":flutter_framework_source",
Expand Down Expand Up @@ -153,6 +157,9 @@ source_set("flutter_framework_source") {
sources += _flutter_framework_headers

defines = [ "FLUTTER_FRAMEWORK=1" ]
if (darwin_extension_safe) {
defines += [ "APPLICATION_EXTENSION_API_ONLY=1" ]
}

if (shell_enable_metal) {
sources += [
Expand Down Expand Up @@ -321,6 +328,10 @@ shared_library("create_flutter_framework_dylib") {

ldflags = [ "-Wl,-install_name,@rpath/Flutter.framework/Flutter" ]

if (darwin_extension_safe) {
ldflags += [ "-fapplication-extension" ]
}

public = _flutter_framework_headers

deps = [
Expand Down Expand Up @@ -411,10 +422,24 @@ copy("copy_license") {
shared_library("copy_and_verify_framework_module") {
framework_search_path = rebase_path("$root_out_dir")
visibility = [ ":*" ]
cflags_objc = [ "-F$framework_search_path" ]
cflags_objc = [
"-F$framework_search_path",
"-fapplication-extension",
]

if (darwin_extension_safe) {
ldflags = [
"-F$framework_search_path",
Copy link
Member

Choose a reason for hiding this comment

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

Surprised this wasn't needed before... maybe copy_and_verify_framework_module isn't actually working (ignoring the extension stuff)?

"-fapplication-extension",
"-Xlinker",
"-fatal_warnings",
]
frameworks = [ "Flutter.framework" ]
deps = [ ":copy_dylib" ]
}

sources = [ "framework/Source/FlutterUmbrellaImport.m" ]
deps = [
deps += [
":copy_framework_headers",
":copy_framework_info_plist",
":copy_framework_module_map",
Expand All @@ -439,7 +464,7 @@ group("universal_flutter_framework") {

action("flutter_framework") {
script = "//flutter/sky/tools/create_xcframework.py"
outputs = [ "$root_out_dir/Flutter.xcframework" ]
outputs = [ "$_flutter_xcframework_dir" ]
args = [
"--frameworks",
rebase_path("$_flutter_framework_dir"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
* code as necessary from FlutterAppDelegate.mm.
*/
FLUTTER_DARWIN_EXPORT
NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions")
@interface FlutterAppDelegate
: UIResponder <UIApplicationDelegate, FlutterPluginRegistry, FlutterAppLifeCycleProvider>

Expand Down
3 changes: 2 additions & 1 deletion shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,8 @@ typedef enum {
*
* @param delegate The receiving object, such as the plugin's main class.
*/
- (void)addApplicationDelegate:(NSObject<FlutterPlugin>*)delegate;
- (void)addApplicationDelegate:(NSObject<FlutterPlugin>*)delegate
NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in plugins used in app extensions");

/**
* Returns the file name for the given asset.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
* Propagates `UIAppDelegate` callbacks to registered plugins.
*/
FLUTTER_DARWIN_EXPORT
NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in plugins used in app extensions")
@interface FlutterPluginAppLifeCycleDelegate : NSObject <UNUserNotificationCenterDelegate>

/**
Expand Down
65 changes: 44 additions & 21 deletions shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,39 @@

static const char* kApplicationKernelSnapshotFileName = "kernel_blob.bin";

NS_INLINE NSBundle* FLTFrameworkBundle() {
NSBundle* mainBundle = [NSBundle mainBundle];
NSBundle* bundle = FLTFrameworkBundleWithIdentifier([FlutterDartProject defaultBundleIdentifier]);

// App extension bundle is in Runner.app/PlugIns/Extension.appex.
if ([mainBundle.bundleURL.pathExtension isEqualToString:@"appex"]) {
// Up two levels.
NSBundle* appBundle =
[NSBundle bundleWithURL:mainBundle.bundleURL.URLByDeletingLastPathComponent
.URLByDeletingLastPathComponent];
bundle = FLTFrameworkBundleInternal([FlutterDartProject defaultBundleIdentifier],
appBundle.privateFrameworksURL);
}

if (bundle == nil) {
bundle = mainBundle;
}

return bundle;
}

NS_INLINE NSURL* FLTAssetsFromBundle(NSBundle* bundle) {
NSString* assetsPathFromInfoPlist = [bundle objectForInfoDictionaryKey:@"FLTAssetsPath"];
NSString* flutterAssetsName =
assetsPathFromInfoPlist != nil ? assetsPathFromInfoPlist : @"flutter_assets";
NSURL* assets = [bundle URLForResource:flutterAssetsName withExtension:nil];

if ([assets checkResourceIsReachableAndReturnError:NULL]) {
return assets;
}
return nil;
}

flutter::Settings FLTDefaultSettingsForBundle(NSBundle* bundle, NSProcessInfo* processInfoOrNil) {
auto command_line = flutter::CommandLineFromNSProcessInfo(processInfoOrNil);

Expand All @@ -46,10 +79,7 @@

bool hasExplicitBundle = bundle != nil;
if (bundle == nil) {
bundle = FLTFrameworkBundleWithIdentifier([FlutterDartProject defaultBundleIdentifier]);
}
if (bundle == nil) {
bundle = mainBundle;
bundle = FLTFrameworkBundle();
}

auto settings = flutter::SettingsFromCommandLine(command_line);
Expand Down Expand Up @@ -122,29 +152,24 @@

// Checks to see if the flutter assets directory is already present.
if (settings.assets_path.empty()) {
NSString* assetsName = [FlutterDartProject flutterAssetsName:bundle];
NSString* assetsPath = [bundle pathForResource:assetsName ofType:@""];
NSURL* assetsURL = FLTAssetsFromBundle(bundle);

if (assetsPath.length == 0) {
assetsPath = [mainBundle pathForResource:assetsName ofType:@""];
}

if (assetsPath.length == 0) {
NSLog(@"Failed to find assets path for \"%@\"", assetsName);
if (assetsURL == nil) {
NSLog(@"Failed to find assets path for \"%@\"", bundle);
} else {
settings.assets_path = assetsPath.UTF8String;
settings.assets_path = assetsURL.path.UTF8String;

// Check if there is an application kernel snapshot in the assets directory we could
// potentially use. Looking for the snapshot makes sense only if we have a VM that can use
// it.
if (!flutter::DartVM::IsRunningPrecompiledCode()) {
NSURL* applicationKernelSnapshotURL =
[NSURL URLWithString:@(kApplicationKernelSnapshotFileName)
relativeToURL:[NSURL fileURLWithPath:assetsPath]];
if ([[NSFileManager defaultManager] fileExistsAtPath:applicationKernelSnapshotURL.path]) {
[assetsURL URLByAppendingPathComponent:@(kApplicationKernelSnapshotFileName)];
NSError* error;
if ([applicationKernelSnapshotURL checkResourceIsReachableAndReturnError:&error]) {
settings.application_kernel_asset = applicationKernelSnapshotURL.path.UTF8String;
} else {
NSLog(@"Failed to find snapshot: %@", applicationKernelSnapshotURL.path);
NSLog(@"Failed to find snapshot at %@: %@", applicationKernelSnapshotURL.path, error);
}
}
}
Expand Down Expand Up @@ -331,11 +356,9 @@ - (instancetype)initWithSettings:(const flutter::Settings&)settings {

+ (NSString*)flutterAssetsName:(NSBundle*)bundle {
if (bundle == nil) {
bundle = FLTFrameworkBundleWithIdentifier([FlutterDartProject defaultBundleIdentifier]);
}
if (bundle == nil) {
bundle = [NSBundle mainBundle];
bundle = FLTFrameworkBundle();
}

NSString* flutterAssetsName = [bundle objectForInfoDictionaryKey:@"FLTAssetsPath"];
if (flutterAssetsName == nil) {
flutterAssetsName = @"Frameworks/App.framework/flutter_assets";
Expand Down
6 changes: 5 additions & 1 deletion shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
Original file line number Diff line number Diff line change
Expand Up @@ -850,7 +850,11 @@ - (BOOL)createShell:(NSString*)entrypoint
);

_isGpuDisabled =
#if APPLICATION_EXTENSION_API_ONLY
NO;
#else
[UIApplication sharedApplication].applicationState == UIApplicationStateBackground;
#endif
// Create the shell. This is a blocking operation.
std::unique_ptr<flutter::Shell> shell = flutter::Shell::Create(
/*platform_data=*/platformData,
Expand Down Expand Up @@ -1452,7 +1456,7 @@ - (void)addMethodCallDelegate:(NSObject<FlutterPlugin>*)delegate
}];
}

- (void)addApplicationDelegate:(NSObject<FlutterPlugin>*)delegate {
- (void)addApplicationDelegate:(NSObject<FlutterPlugin>*)delegate NS_EXTENSION_UNAVAILABLE_IOS("") {
id<UIApplicationDelegate> appDelegate = [[UIApplication sharedApplication] delegate];
if ([appDelegate conformsToProtocol:@protocol(FlutterAppLifeCycleProvider)]) {
id<FlutterAppLifeCycleProvider> lifeCycleProvider =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ - (void)setSystemChromeApplicationSwitcherDescription:(NSDictionary*)object {
}

- (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays {
#if !APPLICATION_EXTENSION_API_ONLY
// Checks if the top status bar should be visible. This platform ignores all
// other overlays

Copy link
Member

Choose a reason for hiding this comment

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

I didn't do this in my prototype PR, but ideally we could get rid of as many sharedApplication usages as possible.
-[UIApplication statusBarHidden] is actually deprecated in iOS 13:

@property(readonly, nonatomic,getter=isStatusBarHidden) BOOL statusBarHidden API_UNAVAILABLE(tvos) API_DEPRECATED("Use the statusBarManager property of the window scene instead.", ios(2.0, 13.0));

Can you instead swap this to using the new API when 13 is @available, and fall back to sharedApplication statusBarHidden for older versions only? Probably best to do this in its own review though this can sit on in case it causes issues. I don't trust our testing coverage here.

From the [_engine.get() viewController] I think you could get .viewIfLoaded.window.windowScene?

I'm not sure if it's meaningful to have a status bar in an extension, though.

Expand All @@ -175,9 +176,11 @@ - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays {
postNotificationName:FlutterViewControllerHideHomeIndicator
object:nil];
}
#endif
}

- (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode {
#if !APPLICATION_EXTENSION_API_ONLY
// Checks if the top status bar should be visible, reflected by edge to edge setting. This
// platform ignores all other system ui modes.

Expand All @@ -195,6 +198,7 @@ - (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode {
postNotificationName:FlutterViewControllerHideHomeIndicator
object:nil];
}
#endif
}

- (void)restoreSystemChromeSystemUIOverlays {
Expand Down Expand Up @@ -231,9 +235,11 @@ - (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message {
object:nil
userInfo:@{@(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle)}];
} else {
#if !APPLICATION_EXTENSION_API_ONLY
// Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9
// in favor of delegating to the view controller
[[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle];
Copy link
Member

Choose a reason for hiding this comment

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

setStatusBarStyle was deprecated in iOS 9 so we wouldn't even need a fallback or the macro (minimum is now iOS 11), if we can get a view controller here ([_engine.get() viewController]?)

- (void)setStatusBarStyle:(UIStatusBarStyle)statusBarStyle animated:(BOOL)animated API_DEPRECATED("Use -[UIViewController preferredStatusBarStyle]", ios(2.0, 9.0)) API_UNAVAILABLE(tvos);

Though I don't really understand the delegateToViewController logic or why we wouldn't want to pass it along to the embedder... #2732

#endif
}
}

Expand All @@ -249,11 +255,15 @@ - (void)popSystemNavigator:(BOOL)isAnimated {
if (navigationController) {
[navigationController popViewControllerAnimated:isAnimated];
} else {
#if !APPLICATION_EXTENSION_API_ONLY
UIViewController* rootViewController =
[UIApplication sharedApplication].keyWindow.rootViewController;
if (engineViewController != rootViewController) {
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if we still need to check something here in the share extension to avoid regressing #30939.

#endif
[engineViewController dismissViewControllerAnimated:isAnimated completion:nil];
#if !APPLICATION_EXTENSION_API_ONLY
}
#endif
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -813,9 +813,14 @@ - (void)viewDidAppear:(BOOL)animated {
if ([_engine.get() viewController] == self) {
[self onUserSettingsChanged:nil];
[self onAccessibilityStatusChanged:nil];

#if !APPLICATION_EXTENSION_API_ONLY
if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive) {
#endif
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"];
#if !APPLICATION_EXTENSION_API_ONLY
}
#endif
}
[super viewDidAppear:animated];
}
Expand Down Expand Up @@ -1287,8 +1292,13 @@ - (void)viewDidLayoutSubviews {
// There is no guarantee that UIKit will layout subviews when the application is active. Creating
// the surface when inactive will cause GPU accesses from the background. Only wait for the first
// frame to render when the application is actually active.
bool applicationIsActive =
BOOL applicationIsActive =

#if APPLICATION_EXTENSION_API_ONLY
YES;
#else
[UIApplication sharedApplication].applicationState == UIApplicationStateActive;
#endif

// This must run after updateViewportMetrics so that the surface creation tasks are queued after
// the viewport metrics update tasks.
Expand Down Expand Up @@ -1808,6 +1818,10 @@ - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences {
_orientationPreferences = new_preferences;

if (@available(iOS 16.0, *)) {
#if APPLICATION_EXTENSION_API_ONLY
UIWindowScene* windowScene = self.viewIfLoaded.window.windowScene;
[self performOrientationUpdateOnWindowScene:windowScene];
Comment on lines +1822 to +1823
Copy link
Member

Choose a reason for hiding this comment

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

I think you could just use this logic instead of anything in the #else, I didn't test it though so in my PR I only used it in the APPLICATION_EXTENSION_API_ONLY check.

#else
for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) {
if (![scene isKindOfClass:[UIWindowScene class]]) {
continue;
Expand All @@ -1825,7 +1839,9 @@ - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences {
}];
[self setNeedsUpdateOfSupportedInterfaceOrientations];
}
#endif
} else {
#if !APPLICATION_EXTENSION_API_ONLY
UIInterfaceOrientationMask currentInterfaceOrientation =
1 << [[UIApplication sharedApplication] statusBarOrientation];
Copy link
Member

Choose a reason for hiding this comment

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

If available you could check the self.viewIfLoaded.window.windowScene.statusBarOrientation since this was deprecated in iOS 13.

@property(readonly, nonatomic) UIInterfaceOrientation statusBarOrientation API_UNAVAILABLE(tvos) API_DEPRECATED("Use the interfaceOrientation property of the window scene instead.", ios(2.0, 13.0));

if (!(_orientationPreferences & currentInterfaceOrientation)) {
Expand All @@ -1848,10 +1864,28 @@ - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences {
forKey:@"orientation"];
}
}
#endif
}
}
}

- (void)performOrientationUpdateOnWindowScene:(UIWindowScene*)windowScene API_AVAILABLE(ios(16.0)) {
if (windowScene == nil) {
return;
}

UIWindowSceneGeometryPreferencesIOS* preference = [[UIWindowSceneGeometryPreferencesIOS alloc]
initWithInterfaceOrientations:_orientationPreferences];
[windowScene
requestGeometryUpdateWithPreferences:preference
errorHandler:^(NSError* error) {
os_log_error(OS_LOG_DEFAULT,
"Failed to change device orientation: %@", error);
}];
[self setNeedsUpdateOfSupportedInterfaceOrientations];
[preference release];
}

- (void)onHideHomeIndicatorNotification:(NSNotification*)notification {
self.isHomeIndicatorHidden = YES;
}
Expand Down Expand Up @@ -1951,7 +1985,12 @@ - (void)onUserSettingsChanged:(NSNotification*)notification {
}

- (CGFloat)textScaleFactor {
#if APPLICATION_EXTENSION_API_ONLY
UIContentSizeCategory category =
self.mainScreenIfViewLoaded.traitCollection.preferredContentSizeCategory;
Comment on lines +1989 to +1990
Copy link
Member

Choose a reason for hiding this comment

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

I think you can just use this in all cases since this is all available on iOS 10+, but I didn't test it so I put it in the macro for safety in my PR.

#else
UIContentSizeCategory category = [UIApplication sharedApplication].preferredContentSizeCategory;
#endif
// The delta is computed by approximating Apple's typography guidelines:
// https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/
//
Expand Down
Loading