From 6d14eb21292c0c7ed16bce2d8f9c092cf049338a Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Wed, 21 May 2025 15:01:28 -0700 Subject: [PATCH] [Reland] Implements UISceneDelegate dynamically w/ FlutterLaunchEngine (#168396) (#168914) ## **BREAKING CHANGE** Adopting Apple's UISceneDelegate protocol shifts the initialization order of apps. For the common cases we've made sure they will work without change. The one case that will require a change is any app that in `-[UIApplicateDelegate didFinishLaunchingWithOptions:]` assumes that `UIApplicationDelegate.window.rootViewController` is a `FlutterViewController` instance. This is sometimes done to register platform channels directly on the `FlutterViewController`. Instead users should use the `FlutterPluginRegistry` API's to create platform channels in `-[UIApplicateDelegate didFinishLaunchingWithOptions:]`, like `FlutterPlugin`s do. An example can be seen here: https://github.com/flutter/flutter/pull/168914/files#diff-9f59c5248b58124beca7e290a57646023cda3ca024607092c6c6932606ce16ee ## Changes since revert Device lab tests have been migrated to using the FlutterPlugin API for creating platform channels at process launch. ## Description fixes: https://github.com/flutter/flutter/issues/167267 design doc: https://docs.google.com/document/d/1ZfcQOs-UKRa9jsFG84-MTFeibZTLKCvPQLxF2eskx44/edit?tab=t.0 relands https://github.com/flutter/flutter/pull/168396 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --- .../channels/ios/Runner/AppDelegate.m | 18 +++--- .../ios/Runner/AppDelegate.m | 10 +-- .../ci/licenses_golden/licenses_flutter | 8 +++ engine/src/flutter/engine.code-workspace | 31 +++++++++ .../shell/platform/darwin/ios/BUILD.gn | 4 ++ .../framework/Source/FlutterAppDelegate.mm | 63 ++++++++++++++++++- .../Source/FlutterAppDelegateTest.mm | 15 +++++ .../Source/FlutterAppDelegate_Internal.h | 16 +++++ .../framework/Source/FlutterLaunchEngine.h | 40 ++++++++++++ .../framework/Source/FlutterLaunchEngine.m | 52 +++++++++++++++ .../Source/FlutterLaunchEngineTest.mm | 26 ++++++++ .../framework/Source/FlutterViewController.mm | 38 ++++++++--- .../Source/FlutterViewControllerTest.mm | 29 +++++++++ .../ios/IosUnitTests/App/AppDelegate.h | 6 ++ .../ios/IosUnitTests/App/AppDelegate.m | 5 ++ .../ios/IosUnitTests/App/Flutter.storyboard | 33 ++++++++++ .../IosUnitTests.xcodeproj/project.pbxproj | 6 ++ .../vscode_workspace/engine-workspace.yaml | 15 +++++ .../platform_channel/ios/Runner/AppDelegate.m | 7 +-- .../ios/Runner/AppDelegate.swift | 8 +-- 20 files changed, 395 insertions(+), 35 deletions(-) create mode 100644 engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Internal.h create mode 100644 engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.h create mode 100644 engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.m create mode 100644 engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngineTest.mm create mode 100644 engine/src/flutter/testing/ios/IosUnitTests/App/Flutter.storyboard diff --git a/dev/integration_tests/channels/ios/Runner/AppDelegate.m b/dev/integration_tests/channels/ios/Runner/AppDelegate.m index 8fc53c3005c..eaf8df68dd0 100644 --- a/dev/integration_tests/channels/ios/Runner/AppDelegate.m +++ b/dev/integration_tests/channels/ios/Runner/AppDelegate.m @@ -86,38 +86,36 @@ const UInt8 PAIR = 129; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [GeneratedPluginRegistrant registerWithRegistry:self]; // Override point for customization after application launch. - FlutterViewController *flutterController = - (FlutterViewController *)self.window.rootViewController; - + id registrar = [self registrarForPlugin:@"platform-channel-test"]; ExtendedReaderWriter* extendedReaderWriter = [ExtendedReaderWriter new]; [self setupMessagingHandshakeOnChannel: [FlutterBasicMessageChannel messageChannelWithName:@"binary-msg" - binaryMessenger:flutterController + binaryMessenger:registrar.messenger codec:[FlutterBinaryCodec sharedInstance]]]; [self setupMessagingHandshakeOnChannel: [FlutterBasicMessageChannel messageChannelWithName:@"string-msg" - binaryMessenger:flutterController + binaryMessenger:registrar.messenger codec:[FlutterStringCodec sharedInstance]]]; [self setupMessagingHandshakeOnChannel: [FlutterBasicMessageChannel messageChannelWithName:@"json-msg" - binaryMessenger:flutterController + binaryMessenger:registrar.messenger codec:[FlutterJSONMessageCodec sharedInstance]]]; [self setupMessagingHandshakeOnChannel: [FlutterBasicMessageChannel messageChannelWithName:@"std-msg" - binaryMessenger:flutterController + binaryMessenger:registrar.messenger codec:[FlutterStandardMessageCodec codecWithReaderWriter:extendedReaderWriter]]]; [self setupMethodCallSuccessHandshakeOnChannel: [FlutterMethodChannel methodChannelWithName:@"json-method" - binaryMessenger:flutterController + binaryMessenger:registrar.messenger codec:[FlutterJSONMethodCodec sharedInstance]]]; [self setupMethodCallSuccessHandshakeOnChannel: [FlutterMethodChannel methodChannelWithName:@"std-method" - binaryMessenger:flutterController + binaryMessenger:registrar.messenger codec:[FlutterStandardMethodCodec codecWithReaderWriter:extendedReaderWriter]]]; [[FlutterBasicMessageChannel messageChannelWithName:@"std-echo" - binaryMessenger:flutterController + binaryMessenger:registrar.messenger codec:[FlutterStandardMessageCodec codecWithReaderWriter:extendedReaderWriter]] setMessageHandler:^(id message, FlutterReply reply) { diff --git a/dev/integration_tests/external_textures/ios/Runner/AppDelegate.m b/dev/integration_tests/external_textures/ios/Runner/AppDelegate.m index 7ad7dee967c..572b2c62edc 100644 --- a/dev/integration_tests/external_textures/ios/Runner/AppDelegate.m +++ b/dev/integration_tests/external_textures/ios/Runner/AppDelegate.m @@ -21,13 +21,15 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - FlutterViewController* flutterController = - (FlutterViewController*)self.window.rootViewController; + id registrar = [self registrarForPlugin:@"external_texture_test"]; FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"texture" - binaryMessenger:flutterController]; + binaryMessenger:registrar.messenger]; [channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { if ([@"start" isEqualToString:call.method]) { + FlutterViewController* flutterController = + (FlutterViewController*)self.window.rootViewController; + _textureId = [flutterController registerTexture:self]; _framesProduced = 0; _framesConsumed = 0; _frameRate = 1.0 / [(NSNumber*) call.arguments intValue]; @@ -50,7 +52,7 @@ result(FlutterMethodNotImplemented); } }]; - _textureId = [flutterController registerTexture:self]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 21a5621774e..dc459678e6f 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -52778,6 +52778,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/ConnectionCo ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FakeUIPressProxy.swift + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Internal.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Test.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache_Internal.h + ../../../flutter/LICENSE @@ -52808,6 +52809,9 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterKeySe ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManagerTest.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.m + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngineTest.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterMetalLayerTest.mm + ../../../flutter/LICENSE @@ -55804,6 +55808,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/ConnectionColl FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FakeUIPressProxy.swift FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Internal.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Test.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache_Internal.h @@ -55834,6 +55839,9 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterKeySeco FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManagerTest.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.m +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngineTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterMetalLayer.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterMetalLayerTest.mm diff --git a/engine/src/flutter/engine.code-workspace b/engine/src/flutter/engine.code-workspace index 544677d98bf..0d8f5d22f06 100644 --- a/engine/src/flutter/engine.code-workspace +++ b/engine/src/flutter/engine.code-workspace @@ -425,6 +425,37 @@ "ios_debug_unopt" ] }, + { + "type": "shell", + "command": "./flutter/bin/et", + "options": { + "cwd": "${workspaceFolder}/.." + }, + "problemMatcher": [ + "$gcc" + ], + "presentation": { + "echo": true, + "reveal": "silent", + "focus": false, + "panel": "shared", + "clear": true + }, + "group": { + "kind": "build" + }, + "label": "ios_debug_sim_unopt_arm64", + "args": [ + "build", + "-c", + "host_debug_unopt_arm64", + "&&", + "./flutter/bin/et", + "build", + "-c", + "ios_debug_sim_unopt_arm64" + ] + }, { "type": "shell", "command": "./flutter/bin/et", diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index d2297ee1ee1..6ec2bf1bae7 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -75,6 +75,7 @@ source_set("flutter_framework_source") { sources = [ "framework/Source/FlutterAppDelegate.mm", + "framework/Source/FlutterAppDelegate_Internal.h", "framework/Source/FlutterCallbackCache.mm", "framework/Source/FlutterCallbackCache_Internal.h", "framework/Source/FlutterChannelKeyResponder.h", @@ -93,6 +94,8 @@ source_set("flutter_framework_source") { "framework/Source/FlutterKeySecondaryResponder.h", "framework/Source/FlutterKeyboardManager.h", "framework/Source/FlutterKeyboardManager.mm", + "framework/Source/FlutterLaunchEngine.h", + "framework/Source/FlutterLaunchEngine.m", "framework/Source/FlutterMetalLayer.h", "framework/Source/FlutterMetalLayer.mm", "framework/Source/FlutterOverlayView.h", @@ -250,6 +253,7 @@ if (enable_ios_unittests) { "framework/Source/FlutterFakeKeyEvents.h", "framework/Source/FlutterFakeKeyEvents.mm", "framework/Source/FlutterKeyboardManagerTest.mm", + "framework/Source/FlutterLaunchEngineTest.mm", "framework/Source/FlutterMetalLayerTest.mm", "framework/Source/FlutterPlatformPluginTest.mm", "framework/Source/FlutterPlatformViewsTest.mm", diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index a30dd9a6b19..634f37a9586 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -9,6 +9,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Test.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate_internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" @@ -20,9 +21,33 @@ static NSString* const kRemoteNotificationCapabitiliy = @"remote-notification"; static NSString* const kBackgroundFetchCapatibility = @"fetch"; static NSString* const kRestorationStateAppModificationKey = @"mod-date"; +@interface FlutterSceneDelegate : NSObject +@property(nonatomic, strong, nullable) UIWindow* window; +@end + +@implementation FlutterSceneDelegate + +- (void)scene:(UIScene*)scene + willConnectToSession:(UISceneSession*)session + options:(UISceneConnectionOptions*)connectionOptions { + NSObject* appDelegate = FlutterSharedApplication.application.delegate; + if (appDelegate.window.rootViewController) { + // If this is not nil we are running into a case where someone is manually + // performing root view controller setup in the UIApplicationDelegate. + UIWindowScene* windowScene = (UIWindowScene*)scene; + self.window = [[UIWindow alloc] initWithWindowScene:windowScene]; + self.window.rootViewController = appDelegate.window.rootViewController; + appDelegate.window = self.window; + [self.window makeKeyAndVisible]; + } +} + +@end + @interface FlutterAppDelegate () @property(nonatomic, copy) FlutterViewController* (^rootFlutterViewControllerGetter)(void); @property(nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate; +@property(nonatomic, strong) FlutterLaunchEngine* launchEngine; @end @implementation FlutterAppDelegate @@ -30,10 +55,15 @@ static NSString* const kRestorationStateAppModificationKey = @"mod-date"; - (instancetype)init { if (self = [super init]) { _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init]; + _launchEngine = [[FlutterLaunchEngine alloc] init]; } return self; } +- (nullable FlutterEngine*)takeLaunchEngine { + return [self.launchEngine takeEngine]; +} + - (BOOL)application:(UIApplication*)application willFinishLaunchingWithOptions:(NSDictionary*)launchOptions { return [self.lifeCycleDelegate application:application @@ -233,7 +263,7 @@ static NSString* const kRestorationStateAppModificationKey = @"mod-date"; if (flutterRootViewController) { return [[flutterRootViewController pluginRegistry] registrarForPlugin:pluginKey]; } - return nil; + return [self.launchEngine.engine registrarForPlugin:pluginKey]; } - (BOOL)hasPlugin:(NSString*)pluginKey { @@ -241,7 +271,7 @@ static NSString* const kRestorationStateAppModificationKey = @"mod-date"; if (flutterRootViewController) { return [[flutterRootViewController pluginRegistry] hasPlugin:pluginKey]; } - return false; + return [self.launchEngine.engine hasPlugin:pluginKey]; } - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey { @@ -249,7 +279,7 @@ static NSString* const kRestorationStateAppModificationKey = @"mod-date"; if (flutterRootViewController) { return [[flutterRootViewController pluginRegistry] valuePublishedByPlugin:pluginKey]; } - return nil; + return [self.launchEngine.engine valuePublishedByPlugin:pluginKey]; } #pragma mark - Selectors handling @@ -341,4 +371,31 @@ static NSString* const kRestorationStateAppModificationKey = @"mod-date"; return [fileDate timeIntervalSince1970]; } +- (UISceneConfiguration*)application:(UIApplication*)application + configurationForConnectingSceneSession:(UISceneSession*)connectingSceneSession + options:(UISceneConnectionOptions*)options { + NSDictionary* sceneManifest = + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationSceneManifest"]; + NSDictionary* sceneConfigs = sceneManifest[@"UISceneConfigurations"]; + + if (sceneConfigs.count > 0) { + return connectingSceneSession.configuration; + } else { + UISceneConfiguration* config = + [UISceneConfiguration configurationWithName:@"flutter" + sessionRole:connectingSceneSession.role]; + config.delegateClass = [FlutterSceneDelegate class]; + + NSString* mainStoryboard = + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIMainStoryboardFile"]; + + if (mainStoryboard) { + UIStoryboard* storyboard = [UIStoryboard storyboardWithName:mainStoryboard + bundle:[NSBundle mainBundle]]; + config.storyboard = storyboard; + } + return config; + } +} + @end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm index df9e7c9b43a..8966b36fe89 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm @@ -8,6 +8,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Test.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h" @@ -154,6 +155,20 @@ FLUTTER_ASSERT_ARC XCTAssertNil(weakWindow); } +- (void)testGrabLaunchEngine { + // Clear out the mocking of the root view controller. + [self.mockMainBundle stopMocking]; + self.appDelegate.rootFlutterViewControllerGetter = nil; + // Working with plugins forces the creation of an engine. + XCTAssertFalse([self.appDelegate hasPlugin:@"hello"]); + XCTAssertNotNil([self.appDelegate takeLaunchEngine]); + XCTAssertNil([self.appDelegate takeLaunchEngine]); +} + +- (void)testGrabLaunchEngineWithoutPlugins { + XCTAssertNil([self.appDelegate takeLaunchEngine]); +} + #pragma mark - Deep linking - (void)testUniversalLinkPushRouteInformation { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Internal.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Internal.h new file mode 100644 index 00000000000..b90796d7514 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Internal.h @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERAPPDELEGATE_INTERNAL_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERAPPDELEGATE_INTERNAL_H_ + +#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h" + +@interface FlutterAppDelegate () + +- (nullable FlutterEngine*)takeLaunchEngine; + +@end + +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERAPPDELEGATE_INTERNAL_H_ diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.h new file mode 100644 index 00000000000..5a054f47194 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.h @@ -0,0 +1,40 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERLAUNCHENGINE_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERLAUNCHENGINE_H_ + +#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" + +/** + * A lazy container for an engine that will only dispense one engine. + * + * This is used to hold an engine for plugin registration when the + * GeneratedPluginRegistrant is called on a FlutterAppDelegate before the first + * FlutterViewController is set up. This is the typical flow after the + * UISceneDelegate migration. + * + * The launch engine is intended to work only with first FlutterViewController + * instantiated with a NIB since that is the only FlutterEngine that registers + * plugins through the FlutterAppDelegate. + */ +@interface FlutterLaunchEngine : NSObject + +/** + * Accessor for the launch engine. + * + * Getting this may allocate an engine. + */ +@property(nonatomic, strong, nullable, readonly) FlutterEngine* engine; + +/** + * Take ownership of the launch engine. + * + * After this is called `self.engine` and `takeEngine` will always return nil. + */ +- (nullable FlutterEngine*)takeEngine; + +@end + +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERLAUNCHENGINE_H_ diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.m b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.m new file mode 100644 index 00000000000..6dbb6a0b933 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.m @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.h" + +@interface FlutterLaunchEngine () { + BOOL _didTakeEngine; + FlutterEngine* _engine; +} +@end + +@implementation FlutterLaunchEngine + +- (instancetype)init { + self = [super init]; + if (self) { + self->_didTakeEngine = NO; + } + return self; +} + +- (FlutterEngine*)engine { + if (!_didTakeEngine && !_engine) { + // `allowHeadlessExecution` is set to `YES` since that has always been the + // default behavior. Technically, someone could have set it to `NO` in their + // nib and it would be ignored here. There is no documented usage of this + // though. + // `restorationEnabled` is set to `YES` since a FlutterViewController + // without restoration will have a nil restorationIdentifier leading no + // restoration data being saved. So, it is safe to turn this on in the event + // that someone does not want it. + _engine = [[FlutterEngine alloc] initWithName:@"io.flutter" + project:[[FlutterDartProject alloc] init] + allowHeadlessExecution:YES + restorationEnabled:YES]; + // Run engine with default values like initialRoute. Specifying these in + // the FlutterViewController was not supported so it's safe to use the + // defaults. + [_engine run]; + } + return _engine; +} + +- (nullable FlutterEngine*)takeEngine { + FlutterEngine* result = _engine; + _engine = nil; + _didTakeEngine = YES; + return result; +} + +@end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngineTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngineTest.mm new file mode 100644 index 00000000000..e3a28edac0d --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngineTest.mm @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import +#import + +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.h" + +FLUTTER_ASSERT_ARC; + +@interface FlutterLaunchEngineTest : XCTestCase +@end + +@implementation FlutterLaunchEngineTest + +- (void)testSimple { + FlutterLaunchEngine* launchEngine = [[FlutterLaunchEngine alloc] init]; + XCTAssertTrue(launchEngine.engine); + XCTAssertTrue([launchEngine takeEngine]); + XCTAssertFalse(launchEngine.engine); +} + +@end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 7139e13983c..ddaa6dab37b 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -17,11 +17,13 @@ #include "flutter/shell/common/thread_host.h" #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h" #import "flutter/shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterChannelKeyResponder.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyPrimaryResponder.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" @@ -255,15 +257,33 @@ typedef struct MouseState { - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project initialRoute:(nullable NSString*)initialRoute { - // Need the project to get settings for the view. Initializing it here means - // the Engine class won't initialize it later. - if (!project) { - project = [[FlutterDartProject alloc] init]; + id appDelegate = FlutterSharedApplication.application.delegate; + FlutterEngine* engine; + if ([appDelegate respondsToSelector:@selector(takeLaunchEngine)]) { + if (self.nibName) { + // Only grab the launch engine if it was created with a nib. + // FlutterViewControllers created from nibs can't specify their initial + // routes so it's safe to take it. + engine = [appDelegate takeLaunchEngine]; + } else { + // If we registered plugins with a FlutterAppDelegate without a xib, throw + // away the engine that was registered through the FlutterAppDelegate. + // That's not a valid usage of the API. + [appDelegate takeLaunchEngine]; + } + } + if (!engine) { + // Need the project to get settings for the view. Initializing it here means + // the Engine class won't initialize it later. + if (!project) { + project = [[FlutterDartProject alloc] init]; + } + + engine = [[FlutterEngine alloc] initWithName:@"io.flutter" + project:project + allowHeadlessExecution:self.engineAllowHeadlessExecution + restorationEnabled:self.restorationIdentifier != nil]; } - FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"io.flutter" - project:project - allowHeadlessExecution:self.engineAllowHeadlessExecution - restorationEnabled:self.restorationIdentifier != nil]; if (!engine) { return; } @@ -272,7 +292,7 @@ typedef struct MouseState { _engine = engine; _flutterView = [[FlutterView alloc] initWithDelegate:_engine opaque:_viewOpaque - enableWideGamut:project.isWideGamutEnabled]; + enableWideGamut:engine.project.isWideGamutEnabled]; [_engine createShell:nil libraryURI:nil initialRoute:initialRoute]; _engineNeedsLaunch = YES; _ongoingTouches = [[NSMutableSet alloc] init]; diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 2849be7c5f3..84dc39b9421 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -13,6 +13,7 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterHourFormat.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/FlutterAppDelegate_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h" @@ -118,6 +119,10 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; @property(nonatomic, copy, readonly) FlutterSendKeyEvent sendEvent; @end +@interface NSObject (Tests) +@property(nonatomic, strong) FlutterEngine* mockLaunchEngine; +@end + @interface FlutterViewController (Tests) @property(nonatomic, assign) double targetViewInsetBottom; @@ -2471,4 +2476,28 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; [mockVC stopMocking]; } +- (void)testGrabLaunchEngine { + id appDelegate = [[UIApplication sharedApplication] delegate]; + XCTAssertTrue([appDelegate respondsToSelector:@selector(setMockLaunchEngine:)]); + [appDelegate setMockLaunchEngine:self.mockEngine]; + UIStoryboard* storyboard = [UIStoryboard storyboardWithName:@"Flutter" bundle:nil]; + XCTAssertTrue(storyboard); + FlutterViewController* viewController = + (FlutterViewController*)[storyboard instantiateInitialViewController]; + XCTAssertTrue(viewController); + XCTAssertTrue([viewController isKindOfClass:[FlutterViewController class]]); + XCTAssertEqual(viewController.engine, self.mockEngine); + [appDelegate setMockLaunchEngine:nil]; +} + +- (void)testDoesntGrabLaunchEngine { + id appDelegate = [[UIApplication sharedApplication] delegate]; + XCTAssertTrue([appDelegate respondsToSelector:@selector(setMockLaunchEngine:)]); + [appDelegate setMockLaunchEngine:self.mockEngine]; + FlutterViewController* flutterViewController = [[FlutterViewController alloc] init]; + XCTAssertNotNil(flutterViewController.engine); + XCTAssertNotEqual(flutterViewController.engine, self.mockEngine); + [appDelegate setMockLaunchEngine:nil]; +} + @end diff --git a/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.h b/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.h index c987bb4da9e..4f989a76b7c 100644 --- a/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.h +++ b/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.h @@ -7,9 +7,15 @@ #import +@class FlutterEngine; + @interface AppDelegate : UIResponder @property(strong, nonatomic) UIWindow* window; +/** The FlutterEngine that will be served by `takeLaunchEngine`. */ +@property(strong, nonatomic) FlutterEngine* mockLaunchEngine; + +- (FlutterEngine*)takeLaunchEngine; @end diff --git a/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.m b/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.m index 9e22f24ef0b..5d468d47102 100644 --- a/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.m +++ b/engine/src/flutter/testing/ios/IosUnitTests/App/AppDelegate.m @@ -30,4 +30,9 @@ - (void)applicationWillTerminate:(UIApplication*)application { } +- (FlutterEngine*)takeLaunchEngine { + // This is just served up for tests and doesn't actually take ownership. + return _mockLaunchEngine; +} + @end diff --git a/engine/src/flutter/testing/ios/IosUnitTests/App/Flutter.storyboard b/engine/src/flutter/testing/ios/IosUnitTests/App/Flutter.storyboard new file mode 100644 index 00000000000..a6eaed85233 --- /dev/null +++ b/engine/src/flutter/testing/ios/IosUnitTests/App/Flutter.storyboard @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index 1bec57693f4..0b593d58ad1 100644 --- a/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 0D1CE5D8233430F400E5D880 /* FlutterChannelsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D1CE5D7233430F400E5D880 /* FlutterChannelsTest.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; + 0D48E9ED2DCE7B16005474A1 /* Flutter.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D48E9EC2DCE7B16005474A1 /* Flutter.storyboard */; }; + 0D48E9EE2DCE7B16005474A1 /* Flutter.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D48E9EC2DCE7B16005474A1 /* Flutter.storyboard */; }; 0D6AB6B622BB05E100EEE540 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6AB6B522BB05E100EEE540 /* AppDelegate.m */; }; 0D6AB6B922BB05E100EEE540 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6AB6B822BB05E100EEE540 /* ViewController.m */; }; 0D6AB6BC22BB05E100EEE540 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D6AB6BA22BB05E100EEE540 /* Main.storyboard */; }; @@ -55,6 +57,7 @@ 0AC2331924BA71D300A85907 /* FlutterPluginAppLifeCycleDelegateTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterPluginAppLifeCycleDelegateTest.mm; sourceTree = ""; }; 0AC2332124BA71D300A85907 /* FlutterViewControllerTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterViewControllerTest.mm; sourceTree = ""; }; 0D1CE5D7233430F400E5D880 /* FlutterChannelsTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterChannelsTest.m; sourceTree = ""; }; + 0D48E9EC2DCE7B16005474A1 /* Flutter.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Flutter.storyboard; sourceTree = ""; }; 0D6AB6B122BB05E100EEE540 /* IosUnitTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IosUnitTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0D6AB6B422BB05E100EEE540 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 0D6AB6B522BB05E100EEE540 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -162,6 +165,7 @@ 0D6AB6BF22BB05E200EEE540 /* LaunchScreen.storyboard */, 0D6AB6C222BB05E200EEE540 /* Info.plist */, 0D6AB6C322BB05E200EEE540 /* main.m */, + 0D48E9EC2DCE7B16005474A1 /* Flutter.storyboard */, ); path = App; sourceTree = ""; @@ -270,6 +274,7 @@ 0D6AB6C122BB05E200EEE540 /* LaunchScreen.storyboard in Resources */, 0D6AB6BE22BB05E200EEE540 /* Assets.xcassets in Resources */, 0D6AB6BC22BB05E100EEE540 /* Main.storyboard in Resources */, + 0D48E9ED2DCE7B16005474A1 /* Flutter.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -277,6 +282,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0D48E9EE2DCE7B16005474A1 /* Flutter.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/engine/src/flutter/tools/vscode_workspace/engine-workspace.yaml b/engine/src/flutter/tools/vscode_workspace/engine-workspace.yaml index 57c837ef677..86291b0b965 100644 --- a/engine/src/flutter/tools/vscode_workspace/engine-workspace.yaml +++ b/engine/src/flutter/tools/vscode_workspace/engine-workspace.yaml @@ -118,6 +118,10 @@ settings: '*.ipp': cpp csetjmp: cpp cfenv: cpp + execution: cpp + print: cpp + source_location: cpp + syncstream: cpp C_Cpp.default.includePath: - ${default} - ${workspaceFolder}/.. @@ -252,6 +256,17 @@ tasks: - build - -c - ios_debug_unopt + - <<: *et-task + label: ios_debug_sim_unopt_arm64 + args: + - build + - -c + - host_debug_unopt_arm64 + - "&&" + - *et-cmd + - build + - -c + - ios_debug_sim_unopt_arm64 - <<: *et-task label: android_debug_unopt_arm64 args: diff --git a/examples/platform_channel/ios/Runner/AppDelegate.m b/examples/platform_channel/ios/Runner/AppDelegate.m index 31512fc8c78..26e74323716 100644 --- a/examples/platform_channel/ios/Runner/AppDelegate.m +++ b/examples/platform_channel/ios/Runner/AppDelegate.m @@ -13,12 +13,11 @@ - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { [GeneratedPluginRegistrant registerWithRegistry:self]; - FlutterViewController* controller = - (FlutterViewController*)self.window.rootViewController; + NSObject* registrar = [self registrarForPlugin:@"battery"]; FlutterMethodChannel* batteryChannel = [FlutterMethodChannel methodChannelWithName:@"samples.flutter.io/battery" - binaryMessenger:controller]; + binaryMessenger:registrar.messenger]; __weak typeof(self) weakSelf = self; [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { @@ -38,7 +37,7 @@ FlutterEventChannel* chargingChannel = [FlutterEventChannel eventChannelWithName:@"samples.flutter.io/charging" - binaryMessenger:controller]; + binaryMessenger:registrar.messenger]; [chargingChannel setStreamHandler:self]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/examples/platform_channel_swift/ios/Runner/AppDelegate.swift b/examples/platform_channel_swift/ios/Runner/AppDelegate.swift index 83b04716f91..8d7c4f40d05 100644 --- a/examples/platform_channel_swift/ios/Runner/AppDelegate.swift +++ b/examples/platform_channel_swift/ios/Runner/AppDelegate.swift @@ -27,11 +27,9 @@ enum MyFlutterErrorCode { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { GeneratedPluginRegistrant.register(with: self) - guard let controller = window?.rootViewController as? FlutterViewController else { - fatalError("rootViewController is not type FlutterViewController") - } + let registry = self.registrar(forPlugin: "battery") let batteryChannel = FlutterMethodChannel(name: ChannelName.battery, - binaryMessenger: controller.binaryMessenger) + binaryMessenger: registry!.messenger()) batteryChannel.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in guard call.method == "getBatteryLevel" else { @@ -42,7 +40,7 @@ enum MyFlutterErrorCode { }) let chargingChannel = FlutterEventChannel(name: ChannelName.charging, - binaryMessenger: controller.binaryMessenger) + binaryMessenger: registry!.messenger()) chargingChannel.setStreamHandler(self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) }