diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 1c0e2765a2c13..742e75fe30785 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -923,6 +923,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine. FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterHeadlessDartRunner.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.mm diff --git a/lib/ui/window.dart b/lib/ui/window.dart index be542150385de..3fb31bf51ec00 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -1056,23 +1056,19 @@ class Window { /// /// ## Android /// - /// On Android, calling - /// [`FlutterView.setInitialRoute`](/javadoc/io/flutter/view/FlutterView.html#setInitialRoute-java.lang.String-) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `createFlutterView` method in your `FlutterActivity` - /// subclass is a suitable time to set the value. The application's - /// `AndroidManifest.xml` file must also be updated to have a suitable - /// [``](https://developer.android.com/guide/topics/manifest/intent-filter-element.html). + /// On Android, the initial route can be set on the [initialRoute](/javadoc/io/flutter/embedding/android/FlutterActivity.NewEngineIntentBuilder.html#initialRoute-java.lang.String-) + /// method of the [FlutterActivity](/javadoc/io/flutter/embedding/android/FlutterActivity.html)'s + /// intent builder. + /// + /// On a standalone engine, see https://flutter.dev/docs/development/add-to-app/android/add-flutter-screen#initial-route-with-a-cached-engine. /// /// ## iOS /// - /// On iOS, calling - /// [`FlutterViewController.setInitialRoute`](/objcdoc/Classes/FlutterViewController.html#/c:objc%28cs%29FlutterViewController%28im%29setInitialRoute:) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `application:didFinishLaunchingWithOptions:` method is a - /// suitable time to set this value. + /// On iOS, the initial route can be set on the `initialRoute` + /// parameter of the [FlutterViewController](/objcdoc/Classes/FlutterViewController.html)'s + /// initializer. + /// + /// On a standalone engine, see https://flutter.dev/docs/development/add-to-app/ios/add-flutter-screen#route. /// /// See also: /// diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index ae795912a6539..b004a95782c9f 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -710,26 +710,6 @@ abstract class Window { /// /// This will be the string "`/`" if no particular route was requested. /// - /// ## Android - /// - /// On Android, calling - /// [`FlutterView.setInitialRoute`](/javadoc/io/flutter/view/FlutterView.html#setInitialRoute-java.lang.String-) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `createFlutterView` method in your `FlutterActivity` - /// subclass is a suitable time to set the value. The application's - /// `AndroidManifest.xml` file must also be updated to have a suitable - /// [``](https://developer.android.com/guide/topics/manifest/intent-filter-element.html). - /// - /// ## iOS - /// - /// On iOS, calling - /// [`FlutterViewController.setInitialRoute`](/objcdoc/Classes/FlutterViewController.html#/c:objc%28cs%29FlutterViewController%28im%29setInitialRoute:) - /// will set this value. The value must be set sufficiently early, i.e. before - /// the [runApp] call is executed in Dart, for this to have any effect on the - /// framework. The `application:didFinishLaunchingWithOptions:` method is a - /// suitable time to set this value. - /// /// See also: /// /// * [Navigator], a widget that handles routing. diff --git a/shell/platform/darwin/ios/framework/Headers/Flutter.h b/shell/platform/darwin/ios/framework/Headers/Flutter.h index 9135c8200603c..d91eba7576fec 100644 --- a/shell/platform/darwin/ios/framework/Headers/Flutter.h +++ b/shell/platform/darwin/ios/framework/Headers/Flutter.h @@ -5,52 +5,6 @@ #ifndef FLUTTER_FLUTTER_H_ #define FLUTTER_FLUTTER_H_ -/** - BREAKING CHANGES: - - December 17, 2018: - - Changed designated initializer on FlutterEngine - - October 5, 2018: - - Removed FlutterNavigationController.h/.mm - - Changed return signature of `FlutterDartHeadlessCodeRunner.run*` from void - to bool - - Removed HeadlessPlatformViewIOS - - Marked FlutterDartHeadlessCodeRunner deprecated - - August 31, 2018: Marked -[FlutterDartProject - initFromDefaultSourceForConfiguration] and FlutterStandardBigInteger as - unavailable. - - July 26, 2018: Marked -[FlutterDartProject - initFromDefaultSourceForConfiguration] deprecated. - - February 28, 2018: Removed "initWithFLXArchive" and - "initWithFLXArchiveWithScriptSnapshot". - - January 15, 2018: Marked "initWithFLXArchive" and - "initWithFLXArchiveWithScriptSnapshot" as unavailable following the - deprecation from December 11, 2017. Scheduled to be removed on February - 19, 2018. - - January 09, 2018: Deprecated "FlutterStandardBigInteger" and its use in - "FlutterStandardMessageCodec" and "FlutterStandardMethodCodec". Scheduled to - be marked as unavailable once the deprecation has been available on the - flutter/flutter alpha branch for four weeks. "FlutterStandardBigInteger" was - needed because the Dart 1.0 int type had no size limit. With Dart 2.0, the - int type is a fixed-size, 64-bit signed integer. If you need to communicate - larger integers, use NSString encoding instead. - - December 11, 2017: Deprecated "initWithFLXArchive" and - "initWithFLXArchiveWithScriptSnapshot" and scheculed the same to be marked as - unavailable on January 15, 2018. Instead, "initWithFlutterAssets" and - "initWithFlutterAssetsWithScriptSnapshot" should be used. The reason for this - change is that the FLX archive will be deprecated and replaced with a flutter - assets directory containing the same files as the FLX did. - - November 29, 2017: Added a BREAKING CHANGES section. - */ - #include "FlutterAppDelegate.h" #include "FlutterBinaryMessenger.h" #include "FlutterCallbackCache.h" diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h index 46980d609a078..87b7753317e73 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h @@ -24,6 +24,11 @@ NS_ASSUME_NONNULL_BEGIN */ extern NSString* const FlutterDefaultDartEntrypoint; +/** + * The default Flutter initial route ("/"). + */ +extern NSString* const FlutterDefaultInitialRoute; + /** * The FlutterEngine class coordinates a single instance of execution for a * `FlutterDartProject`. It may have zero or one `FlutterViewController` at a @@ -53,6 +58,24 @@ extern NSString* const FlutterDefaultDartEntrypoint; FLUTTER_EXPORT @interface FlutterEngine : NSObject +/** + * Default initializer for a FlutterEngine. + * + * Threads created by this FlutterEngine will appear as "FlutterEngine #" in + * Instruments. The prefix can be customized using `initWithName`. + * + * The engine will execute the project located in the bundle with the identifier + * "io.flutter.flutter.app" (the default for Flutter projects). + * + * A newly initialized engine will not run until either `-runWithEntrypoint:` or + * `-runWithEntrypoint:libraryURI:` is called. + * + * FlutterEngine created with this method will have allowHeadlessExecution set to `YES`. + * This means that the engine will continue to run regardless of whether a `FlutterViewController` + * is attached to it or not, until `-destroyContext:` is called or the process finishes. + */ +- (instancetype)init; + /** * Initialize this FlutterEngine. * @@ -114,17 +137,12 @@ FLUTTER_EXPORT project:(nullable FlutterDartProject*)project allowHeadlessExecution:(BOOL)allowHeadlessExecution NS_DESIGNATED_INITIALIZER; -/** - * The default initializer is not available for this object. - * Callers must use `-[FlutterEngine initWithName:project:]`. - */ -- (instancetype)init NS_UNAVAILABLE; - + (instancetype)new NS_UNAVAILABLE; /** * Runs a Dart program on an Isolate from the main Dart library (i.e. the library that - * contains `main()`), using `main()` as the entrypoint (the default for Flutter projects). + * contains `main()`), using `main()` as the entrypoint (the default for Flutter projects), + * and using "/" (the default route) as the initial route. * * The first call to this method will create a new Isolate. Subsequent calls will return * immediately and have no effect. @@ -135,7 +153,7 @@ FLUTTER_EXPORT /** * Runs a Dart program on an Isolate from the main Dart library (i.e. the library that - * contains `main()`). + * contains `main()`), using "/" (the default route) as the initial route. * * The first call to this method will create a new Isolate. Subsequent calls will return * immediately and have no effect. @@ -149,6 +167,25 @@ FLUTTER_EXPORT */ - (BOOL)runWithEntrypoint:(nullable NSString*)entrypoint; +/** + * Runs a Dart program on an Isolate from the main Dart library (i.e. the library that + * contains `main()`). + * + * The first call to this method will create a new Isolate. Subsequent calls will return + * immediately and have no effect. + * + * @param entrypoint The name of a top-level function from the same Dart + * library that contains the app's main() function. If this is FlutterDefaultDartEntrypoint (or + * nil), it will default to `main()`. If it is not the app's main() function, that function must + * be decorated with `@pragma(vm:entry-point)` to ensure the method is not tree-shaken by the Dart + * compiler. + * @param initialRoute The name of the initial Flutter `Navigator` `Route` to load. If this is + * FlutterDefaultInitialRoute (or nil), it will default to the "/" route. + * @return YES if the call succeeds in creating and running a Flutter Engine instance; NO otherwise. + */ +- (BOOL)runWithEntrypoint:(nullable NSString*)entrypoint + initialRoute:(nullable NSString*)initialRoute; + /** * Runs a Dart program on an Isolate using the specified entrypoint and Dart library, * which may not be the same as the library containing the Dart program's `main()` function. diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h index 6f434af1047f7..4468ce7ea770c 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h @@ -55,7 +55,7 @@ FLUTTER_EXPORT * * The initialized viewcontroller will attach itself to the engine as part of this process. * - * @param engine The `FlutterEngine` instance to attach to. + * @param engine The `FlutterEngine` instance to attach to. Cannot be nil. * @param nibName The NIB name to initialize this UIViewController with. * @param nibBundle The NIB bundle. */ @@ -78,6 +78,23 @@ FLUTTER_EXPORT nibName:(nullable NSString*)nibName bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER; +/** + * Initializes a new FlutterViewController and `FlutterEngine` with the specified + * `FlutterDartProject` and `initialRoute`. + * + * This will implicitly create a new `FlutterEngine` which is retrievable via the `engine` property + * after initialization. + * + * @param project The `FlutterDartProject` to initialize the `FlutterEngine` with. + * @param initialRoute The initial `Navigator` route to load. + * @param nibName The NIB name to initialize this UIViewController with. + * @param nibBundle The NIB bundle. + */ +- (instancetype)initWithProject:(nullable FlutterDartProject*)project + initialRoute:(nullable NSString*)initialRoute + nibName:(nullable NSString*)nibName + bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER; + /** * Initializer that is called from loading a FlutterViewController from a XIB. * @@ -117,6 +134,8 @@ FLUTTER_EXPORT - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package; /** + * Deprecated API to set initial route. + * * Attempts to set the first route that the Flutter app shows if the Flutter * runtime hasn't yet started. The default is "/". * @@ -127,9 +146,15 @@ FLUTTER_EXPORT * Setting this after the Flutter started running has no effect. See `pushRoute` * and `popRoute` to change the route after Flutter started running. * + * This is deprecated because it needs to be called at the time of initialization + * and thus should just be in the `initWithProject` initializer. If using + * `initWithEngine`, the initial route should be set on the engine's + * initializer. + * * @param route The name of the first route to show. */ -- (void)setInitialRoute:(NSString*)route; +- (void)setInitialRoute:(NSString*)route + FLUTTER_DEPRECATED("Use FlutterViewController initializer to specify initial route"); /** * Instructs the Flutter Navigator (if any) to go back. @@ -138,8 +163,7 @@ FLUTTER_EXPORT /** * Instructs the Flutter Navigator (if any) to push a route on to the navigation - * stack. The setInitialRoute method should be preferred if this is called before the - * FlutterViewController has come into view. + * stack. * * @param route The name of the route to push to the navigation stack. */ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 51a741fa4109f..8bbb81f5f61da 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -33,6 +33,7 @@ #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" NSString* const FlutterDefaultDartEntrypoint = nil; +NSString* const FlutterDefaultInitialRoute = nil; static constexpr int kNumProfilerSamplesPerSec = 5; @interface FlutterEngineRegistrar : NSObject @@ -47,6 +48,7 @@ @interface FlutterEngine () @property(nonatomic, readonly) NSMutableDictionary* registrars; @property(nonatomic, readwrite, copy) NSString* isolateId; +@property(nonatomic, copy) NSString* initialRoute; @property(nonatomic, retain) id flutterViewControllerWillDeallocObserver; @end @@ -83,6 +85,10 @@ @implementation FlutterEngine { std::unique_ptr _connections; } +- (instancetype)init { + return [self initWithName:@"FlutterEngine" project:nil allowHeadlessExecution:YES]; +} + - (instancetype)initWithName:(NSString*)labelPrefix { return [self initWithName:labelPrefix project:nil allowHeadlessExecution:YES]; } @@ -161,6 +167,7 @@ - (void)dealloc { }]; [_labelPrefix release]; + [_initialRoute release]; [_pluginPublications release]; [_registrars release]; _binaryMessenger.parent = nil; @@ -368,6 +375,13 @@ - (void)setupChannels { binaryMessenger:self.binaryMessenger codec:[FlutterJSONMethodCodec sharedInstance]]); + if ([_initialRoute length] > 0) { + // Flutter isn't ready to receive this method call yet but the channel buffer will cache this. + [_navigationChannel invokeMethod:@"setInitialRoute" arguments:_initialRoute]; + [_initialRoute release]; + _initialRoute = nil; + } + _platformChannel.reset([[FlutterMethodChannel alloc] initWithName:@"flutter/platform" binaryMessenger:self.binaryMessenger @@ -437,13 +451,16 @@ - (void)launchEngine:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil { libraryOrNil:libraryOrNil]); } -- (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { +- (BOOL)createShell:(NSString*)entrypoint + libraryURI:(NSString*)libraryURI + initialRoute:(NSString*)initialRoute { if (_shell != nullptr) { FML_LOG(WARNING) << "This FlutterEngine was already invoked."; return NO; } static size_t shellCount = 1; + self.initialRoute = initialRoute; auto settings = [_dartProject.get() settings]; auto platformData = [_dartProject.get() defaultPlatformData]; @@ -553,21 +570,35 @@ - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { } - (BOOL)run { - return [self runWithEntrypoint:FlutterDefaultDartEntrypoint libraryURI:nil]; + return [self runWithEntrypoint:FlutterDefaultDartEntrypoint + libraryURI:nil + initialRoute:FlutterDefaultInitialRoute]; } - (BOOL)runWithEntrypoint:(NSString*)entrypoint libraryURI:(NSString*)libraryURI { - if ([self createShell:entrypoint libraryURI:libraryURI]) { + return [self runWithEntrypoint:entrypoint + libraryURI:libraryURI + initialRoute:FlutterDefaultInitialRoute]; +} + +- (BOOL)runWithEntrypoint:(NSString*)entrypoint { + return [self runWithEntrypoint:entrypoint libraryURI:nil initialRoute:FlutterDefaultInitialRoute]; +} + +- (BOOL)runWithEntrypoint:(NSString*)entrypoint initialRoute:(NSString*)initialRoute { + return [self runWithEntrypoint:entrypoint libraryURI:nil initialRoute:initialRoute]; +} + +- (BOOL)runWithEntrypoint:(NSString*)entrypoint + libraryURI:(NSString*)libraryURI + initialRoute:(NSString*)initialRoute { + if ([self createShell:entrypoint libraryURI:libraryURI initialRoute:initialRoute]) { [self launchEngine:entrypoint libraryURI:libraryURI]; } return _shell != nullptr; } -- (BOOL)runWithEntrypoint:(NSString*)entrypoint { - return [self runWithEntrypoint:entrypoint libraryURI:nil]; -} - - (void)notifyLowMemory { if (_shell) { _shell->NotifyLowMemoryWarning(); @@ -670,6 +701,15 @@ - (void)showAutocorrectionPromptRectForStart:(NSUInteger)start return _binaryMessenger; } +// For test only. Ideally we should create a dependency injector for all dependencies and +// remove this. +- (void)setBinaryMessenger:(FlutterBinaryMessengerRelay*)binaryMessenger { + // Discard the previous messenger and keep the new one. + _binaryMessenger.parent = nil; + [_binaryMessenger release]; + _binaryMessenger = [binaryMessenger retain]; +} + #pragma mark - FlutterBinaryMessenger - (void)sendOnChannel:(NSString*)channel message:(NSData*)message { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 7fe3cb16e0775..e7a68f9a7d2bd 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -5,7 +5,8 @@ #import #import #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" -#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h" FLUTTER_ASSERT_ARC @@ -79,4 +80,23 @@ - (void)testNotifyPluginOfDealloc { OCMVerify([plugin detachFromEngineForRegistrar:[OCMArg any]]); } +- (void)testRunningInitialRouteSendsNavigationMessage { + id mockBinaryMessenger = OCMClassMock([FlutterBinaryMessengerRelay class]); + + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine setBinaryMessenger:mockBinaryMessenger]; + + // Run with an initial route. + [engine runWithEntrypoint:FlutterDefaultDartEntrypoint initialRoute:@"test"]; + + // Now check that an encoded method call has been made on the binary messenger to set the + // initial route to "test". + FlutterMethodCall* setInitialRouteMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"setInitialRoute" arguments:@"test"]; + NSData* encodedSetInitialRouteMethod = + [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:setInitialRouteMethodCall]; + OCMVerify([mockBinaryMessenger sendOnChannel:@"flutter/navigation" + message:encodedSetInitialRouteMethod]); +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h index 52558eaf71ab3..93e6cbac58514 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h @@ -43,7 +43,9 @@ - (flutter::FlutterPlatformViewsController*)platformViewsController; - (FlutterTextInputPlugin*)textInputPlugin; - (void)launchEngine:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil; -- (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil; +- (BOOL)createShell:(NSString*)entrypoint + libraryURI:(NSString*)libraryOrNil + initialRoute:(NSString*)initialRoute; - (void)attachView; - (void)notifyLowMemory; - (flutter::PlatformViewIOS*)iosPlatformView; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h new file mode 100644 index 0000000000000..7be2f68d77b50 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h @@ -0,0 +1,10 @@ +// 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/Headers/FlutterEngine.h" + +// Category to add test-only visibility. +@interface FlutterEngine (Test) +- (void)setBinaryMessenger:(FlutterBinaryMessengerRelay*)binaryMessenger; +@end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index d788b461586f6..6114281d8695b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -111,26 +111,24 @@ - (instancetype)initWithEngine:(FlutterEngine*)engine return self; } -- (void)sharedSetupWithProject:(nullable FlutterDartProject*)project { - _viewOpaque = YES; - _weakFactory = std::make_unique>(self); - _engine.reset([[FlutterEngine alloc] initWithName:@"io.flutter" - project:project - allowHeadlessExecution:self.engineAllowHeadlessExecution]); - _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); - [_engine.get() createShell:nil libraryURI:nil]; - _engineNeedsLaunch = YES; - _ongoingTouches = [[NSMutableSet alloc] init]; - [self loadDefaultSplashScreenView]; - [self performCommonViewControllerInitialization]; +- (instancetype)initWithProject:(FlutterDartProject*)project + nibName:(NSString*)nibName + bundle:(NSBundle*)nibBundle { + self = [super initWithNibName:nibName bundle:nibBundle]; + if (self) { + [self sharedSetupWithProject:project initialRoute:nil]; + } + + return self; } -- (instancetype)initWithProject:(nullable FlutterDartProject*)project - nibName:(nullable NSString*)nibName - bundle:(nullable NSBundle*)nibBundle { +- (instancetype)initWithProject:(FlutterDartProject*)project + initialRoute:(NSString*)initialRoute + nibName:(NSString*)nibName + bundle:(NSBundle*)nibBundle { self = [super initWithNibName:nibName bundle:nibBundle]; if (self) { - [self sharedSetupWithProject:project]; + [self sharedSetupWithProject:project initialRoute:initialRoute]; } return self; @@ -148,7 +146,7 @@ - (instancetype)initWithCoder:(NSCoder*)aDecoder { - (void)awakeFromNib { [super awakeFromNib]; if (!_engine.get()) { - [self sharedSetupWithProject:nil]; + [self sharedSetupWithProject:nil initialRoute:nil]; } } @@ -156,6 +154,21 @@ - (instancetype)init { return [self initWithProject:nil nibName:nil bundle:nil]; } +- (void)sharedSetupWithProject:(nullable FlutterDartProject*)project + initialRoute:(nullable NSString*)initialRoute { + _viewOpaque = YES; + _weakFactory = std::make_unique>(self); + _engine.reset([[FlutterEngine alloc] initWithName:@"io.flutter" + project:project + allowHeadlessExecution:self.engineAllowHeadlessExecution]); + _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); + [_engine.get() createShell:nil libraryURI:nil initialRoute:initialRoute]; + _engineNeedsLaunch = YES; + _ongoingTouches = [[NSMutableSet alloc] init]; + [self loadDefaultSplashScreenView]; + [self performCommonViewControllerInitialization]; +} + - (BOOL)isViewOpaque { return _viewOpaque; } @@ -469,7 +482,12 @@ - (UIView*)splashScreenFromStoryboard:(NSString*)name { } - (UIView*)splashScreenFromXib:(NSString*)name { - NSArray* objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil]; + NSArray* objects = nil; + @try { + objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil]; + } @catch (NSException* exception) { + return nil; + } if ([objects count] != 0) { UIView* view = [objects objectAtIndex:0]; return view; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index e508d031eb265..2418ea58bc72b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -13,7 +13,9 @@ FLUTTER_ASSERT_ARC @interface FlutterEngine () -- (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI; +- (BOOL)createShell:(NSString*)entrypoint + libraryURI:(NSString*)libraryURI + initialRoute:(NSString*)initialRoute; @end @interface FlutterEngine (TestLowMemory) @@ -513,7 +515,7 @@ - (void)testWillDeallocNotification { - (void)testDoesntLoadViewInInit { FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; - [engine createShell:@"" libraryURI:@""]; + [engine createShell:@"" libraryURI:@"" initialRoute:nil]; FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; @@ -523,7 +525,7 @@ - (void)testDoesntLoadViewInInit { - (void)testHideOverlay { FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; - [engine createShell:@"" libraryURI:@""]; + [engine createShell:@"" libraryURI:@"" initialRoute:nil]; FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; diff --git a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index 598624564fe53..eeaef513460a9 100644 --- a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -28,6 +28,15 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0AC232F424BA71D300A85907 /* SemanticsObjectTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SemanticsObjectTest.mm; sourceTree = ""; }; + 0AC232F724BA71D300A85907 /* FlutterEngineTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterEngineTest.mm; sourceTree = ""; }; + 0AC2330324BA71D300A85907 /* accessibility_bridge_test.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = accessibility_bridge_test.mm; sourceTree = ""; }; + 0AC2330B24BA71D300A85907 /* FlutterTextInputPluginTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterTextInputPluginTest.m; sourceTree = ""; }; + 0AC2330F24BA71D300A85907 /* FlutterBinaryMessengerRelayTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterBinaryMessengerRelayTest.mm; sourceTree = ""; }; + 0AC2331024BA71D300A85907 /* connection_collection_test.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = connection_collection_test.mm; sourceTree = ""; }; + 0AC2331224BA71D300A85907 /* FlutterEnginePlatformViewTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterEnginePlatformViewTest.mm; sourceTree = ""; }; + 0AC2331924BA71D300A85907 /* FlutterPluginAppLifeCycleDelegateTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterPluginAppLifeCycleDelegateTest.m; 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 = ""; }; 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 = ""; }; @@ -62,6 +71,23 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0AC232E924BA71D300A85907 /* Source */ = { + isa = PBXGroup; + children = ( + 0AC232F424BA71D300A85907 /* SemanticsObjectTest.mm */, + 0AC232F724BA71D300A85907 /* FlutterEngineTest.mm */, + 0AC2330324BA71D300A85907 /* accessibility_bridge_test.mm */, + 0AC2330B24BA71D300A85907 /* FlutterTextInputPluginTest.m */, + 0AC2330F24BA71D300A85907 /* FlutterBinaryMessengerRelayTest.mm */, + 0AC2331024BA71D300A85907 /* connection_collection_test.mm */, + 0AC2331224BA71D300A85907 /* FlutterEnginePlatformViewTest.mm */, + 0AC2331924BA71D300A85907 /* FlutterPluginAppLifeCycleDelegateTest.m */, + 0AC2332124BA71D300A85907 /* FlutterViewControllerTest.mm */, + ); + name = Source; + path = ../../../shell/platform/darwin/ios/framework/Source; + sourceTree = ""; + }; 0D1CE5D62334309900E5D880 /* Source-Common */ = { isa = PBXGroup; children = ( @@ -74,6 +100,7 @@ 0D6AB6A822BB05E100EEE540 = { isa = PBXGroup; children = ( + 0AC232E924BA71D300A85907 /* Source */, 0D6AB6B322BB05E100EEE540 /* App */, 0D6AB6CC22BB05E200EEE540 /* Tests */, 0D6AB6B222BB05E100EEE540 /* Products */, diff --git a/testing/run_tests.py b/testing/run_tests.py index 7df1757ee80b0..b692cd3b9eaf0 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -331,10 +331,11 @@ def AssertExpectedJavaVersion(): def AssertExpectedXcodeVersion(): """Checks that the user has a recent version of Xcode installed""" - EXPECTED_MAJOR_VERSION = '11' + EXPECTED_MAJOR_VERSION = ['11', '12'] version_output = subprocess.check_output(['xcodebuild', '-version']) + match = re.match("Xcode (\d+)", version_output) message = "Xcode must be installed to run the iOS embedding unit tests" - assert "Xcode %s." % EXPECTED_MAJOR_VERSION in version_output, message + assert match.group(1) in EXPECTED_MAJOR_VERSION, message def RunJavaTests(filter, android_variant='android_debug_unopt'): """Runs the Java JUnit unit tests for the Android embedding""" diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index 9c43ef8583a4a..cc4462541c2d5 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 0A57B3BD2323C4BD00DD9521 /* ScreenBeforeFlutter.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A57B3BC2323C4BD00DD9521 /* ScreenBeforeFlutter.m */; }; 0A57B3BF2323C74200DD9521 /* FlutterEngine+ScenariosTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A57B3BE2323C74200DD9521 /* FlutterEngine+ScenariosTest.m */; }; 0A57B3C22323D2D700DD9521 /* AppLifecycleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A57B3C12323D2D700DD9521 /* AppLifecycleTests.m */; }; + 0A97D7C024BA937000050525 /* FlutterViewControllerInitialRouteTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A97D7BF24BA937000050525 /* FlutterViewControllerInitialRouteTest.m */; }; 0D14A3FE239743190013D873 /* golden_platform_view_rotate_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 0D14A3FD239743190013D873 /* golden_platform_view_rotate_iPhone SE_simulator.png */; }; 0D8470A4240F0B1F0030B565 /* StatusBarTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D8470A3240F0B1F0030B565 /* StatusBarTest.m */; }; 0DB781EF22E931BE00E9B371 /* Flutter.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 246B4E4522E3B61000073EBF /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -119,6 +120,7 @@ 0A57B3BE2323C74200DD9521 /* FlutterEngine+ScenariosTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FlutterEngine+ScenariosTest.m"; sourceTree = ""; }; 0A57B3C02323C74D00DD9521 /* FlutterEngine+ScenariosTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FlutterEngine+ScenariosTest.h"; sourceTree = ""; }; 0A57B3C12323D2D700DD9521 /* AppLifecycleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppLifecycleTests.m; sourceTree = ""; }; + 0A97D7BF24BA937000050525 /* FlutterViewControllerInitialRouteTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FlutterViewControllerInitialRouteTest.m; sourceTree = ""; }; 0D14A3FD239743190013D873 /* golden_platform_view_rotate_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_rotate_iPhone SE_simulator.png"; sourceTree = ""; }; 0D8470A2240F0B1F0030B565 /* StatusBarTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StatusBarTest.h; sourceTree = ""; }; 0D8470A3240F0B1F0030B565 /* StatusBarTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StatusBarTest.m; sourceTree = ""; }; @@ -243,6 +245,7 @@ children = ( 248FDFC322FE7CD0009CC7CD /* FlutterEngineTest.m */, 0DB781FC22EA2C0300E9B371 /* FlutterViewControllerTest.m */, + 0A97D7BF24BA937000050525 /* FlutterViewControllerInitialRouteTest.m */, 248D76E522E388380012F0C1 /* Info.plist */, 0A57B3C12323D2D700DD9521 /* AppLifecycleTests.m */, ); @@ -469,6 +472,7 @@ files = ( 0DB7820222EA493B00E9B371 /* FlutterViewControllerTest.m in Sources */, 0A57B3C22323D2D700DD9521 /* AppLifecycleTests.m in Sources */, + 0A97D7C024BA937000050525 /* FlutterViewControllerInitialRouteTest.m in Sources */, 248FDFC422FE7CD0009CC7CD /* FlutterEngineTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m index 82aa5bf670993..cfa70262f2585 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m @@ -11,7 +11,7 @@ - (instancetype)initWithScenario:(NSString*)scenario NSAssert([scenario length] != 0, @"You need to provide a scenario"); self = [self initWithName:[NSString stringWithFormat:@"Test engine for %@", scenario] project:nil]; - [self runWithEntrypoint:nil]; + [self run]; [self.binaryMessenger setMessageHandlerOnChannel:@"waiting_for_status" binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) { diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m index f875764e99e2b..b43c6bfa15bda 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterEngineTest.m @@ -34,7 +34,7 @@ - (void)testChannelSetup { XCTAssertNil(engine.platformChannel); XCTAssertNil(engine.lifecycleChannel); - XCTAssertTrue([engine runWithEntrypoint:nil]); + XCTAssertTrue([engine run]); XCTAssertNotNil(engine.navigationChannel); XCTAssertNotNil(engine.platformChannel); diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m new file mode 100644 index 0000000000000..baf9ad3441960 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerInitialRouteTest.m @@ -0,0 +1,84 @@ +// 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 "AppDelegate.h" + +FLUTTER_ASSERT_ARC + +@interface FlutterViewControllerInitialRouteTest : XCTestCase +@property(nonatomic, strong) FlutterViewController* flutterViewController; +@end + +// This test needs to be in its own file with only one test method because dart:ui +// window's defaultRouteName can only be set once per VM. +@implementation FlutterViewControllerInitialRouteTest + +- (void)setUp { + [super setUp]; + self.continueAfterFailure = NO; +} + +- (void)tearDown { + if (self.flutterViewController) { + XCTestExpectation* vcDismissed = [self expectationWithDescription:@"dismiss"]; + [self.flutterViewController dismissViewControllerAnimated:NO + completion:^{ + [vcDismissed fulfill]; + }]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + } + [super tearDown]; +} + +- (void)testSettingInitialRoute { + self.flutterViewController = + [[FlutterViewController alloc] initWithProject:nil + initialRoute:@"myCustomInitialRoute" + nibName:nil + bundle:nil]; + + NSObject* binaryMessenger = self.flutterViewController.binaryMessenger; + __weak typeof(binaryMessenger) weakBinaryMessenger = binaryMessenger; + + FlutterBinaryMessengerConnection waitingForStatusConnection = [binaryMessenger + setMessageHandlerOnChannel:@"waiting_for_status" + binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) { + FlutterMethodChannel* channel = [FlutterMethodChannel + methodChannelWithName:@"driver" + binaryMessenger:weakBinaryMessenger + codec:[FlutterJSONMethodCodec sharedInstance]]; + [channel invokeMethod:@"set_scenario" arguments:@{@"name" : @"initial_route_reply"}]; + }]; + + XCTestExpectation* customInitialRouteSet = + [self expectationWithDescription:@"Custom initial route was set on the Dart side"]; + FlutterBinaryMessengerConnection initialRoutTestChannelConnection = + [binaryMessenger setMessageHandlerOnChannel:@"initial_route_test_channel" + binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) { + NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:message + options:0 + error:nil]; + NSString* initialRoute = dict[@"method"]; + if ([initialRoute isEqualToString:@"myCustomInitialRoute"]) { + [customInitialRouteSet fulfill]; + } else { + XCTFail(@"Expected initial route to be set to " + @"myCustomInitialRoute. Was set to %@ instead", + initialRoute); + } + }]; + + AppDelegate* appDelegate = (AppDelegate*)UIApplication.sharedApplication.delegate; + UIViewController* rootVC = appDelegate.window.rootViewController; + [rootVC presentViewController:self.flutterViewController animated:NO completion:nil]; + + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + [binaryMessenger cleanupConnection:waitingForStatusConnection]; + [binaryMessenger cleanupConnection:initialRoutTestChannelConnection]; +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m index 15acf3fcd7939..48bab99653e0e 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/FlutterViewControllerTest.m @@ -6,6 +6,8 @@ #import #import "AppDelegate.h" +FLUTTER_ASSERT_ARC + @interface FlutterViewControllerTest : XCTestCase @property(nonatomic, strong) FlutterViewController* flutterViewController; @end @@ -19,7 +21,12 @@ - (void)setUp { - (void)tearDown { if (self.flutterViewController) { - [self.flutterViewController removeFromParentViewController]; + XCTestExpectation* vcDismissed = [self expectationWithDescription:@"dismiss"]; + [self.flutterViewController dismissViewControllerAnimated:NO + completion:^{ + [vcDismissed fulfill]; + }]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; } [super tearDown]; } diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m index 310f43fae4390..61610c06468a8 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/ScenariosTests.m @@ -1,10 +1,6 @@ -// -// ScenariosTests.m -// ScenariosTests -// -// Created by Dan Field on 7/20/19. -// Copyright © 2019 flutter. All rights reserved. -// +// Copyright 2019 The Chromium 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 diff --git a/testing/scenario_app/lib/src/initial_route_reply.dart b/testing/scenario_app/lib/src/initial_route_reply.dart new file mode 100644 index 0000000000000..d53a118e842c9 --- /dev/null +++ b/testing/scenario_app/lib/src/initial_route_reply.dart @@ -0,0 +1,30 @@ +// Copyright 2019 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. + +// @dart = 2.6 +import 'dart:ui'; + +import 'package:scenario_app/src/channel_util.dart'; + +import 'scenario.dart'; + +/// A blank page that just sends back to the platform what the set initial +/// route is. +class InitialRouteReply extends Scenario { + /// Creates the InitialRouteReply. + /// + /// The [window] parameter must not be null. + InitialRouteReply(Window window) + : assert(window != null), + super(window); + + @override + void onBeginFrame(Duration duration) { + sendJsonMethodCall( + window: window, + channel: 'initial_route_test_channel', + method: window.defaultRouteName, + ); + } +} diff --git a/testing/scenario_app/lib/src/scenarios.dart b/testing/scenario_app/lib/src/scenarios.dart index e86b44b5d47e4..6aa553380d34c 100644 --- a/testing/scenario_app/lib/src/scenarios.dart +++ b/testing/scenario_app/lib/src/scenarios.dart @@ -6,6 +6,7 @@ import 'dart:ui'; import 'animated_color_square.dart'; +import 'initial_route_reply.dart'; import 'locale_initialization.dart'; import 'platform_view.dart'; import 'poppable_screen.dart'; @@ -41,6 +42,7 @@ Map _scenarios = { 'platform_view_gesture_reject_after_touches_ended': () => PlatformViewForTouchIOSScenario(window, 'platform view touch', id: _viewId++, accept: false, rejectUntilTouchesEnded: true), 'tap_status_bar': () => TouchesScenario(window), 'text_semantics_focus': () => SendTextFocusScemantics(window), + 'initial_route_reply': () => InitialRouteReply(window), }; Map _currentScenarioParams = {};