// Copyright 2014 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 'dart:async'; import 'dart:io'; import 'package:args/args.dart'; import 'package:flutter_devicelab/framework/framework.dart'; import 'package:flutter_devicelab/framework/host_agent.dart'; import 'package:flutter_devicelab/framework/ios.dart'; import 'package:flutter_devicelab/framework/task_result.dart'; import 'package:flutter_devicelab/framework/utils.dart'; import 'package:path/path.dart' as path; /// This test creates a Flutter module, Flutter plugin, and native iOS app. /// It adds the plugin as a dependency and embeds the Flutter module into the native iOS app. /// Additional scenarios can be added in [Scenarios.scenarios]. /// /// To run this test locally, follow instructions in dev/devicelab/README.md. /// The `--local-engine` and `--local-engine-host` flags can be used to run with a local engine. /// /// `--task-args destination=[/path/to/copy/destination]` can be used to override the destination /// of the generated apps/plugins. /// /// e.g. `../../bin/cache/dart-sdk/bin/dart bin/test_runner.dart test -t module_uiscene_test_ios --local-engine ios_debug_sim_unopt_arm64 --local-engine-host host_debug --task-args destination=/path/to/copy/destination` Future main(List args) async { const kDestination = 'destination'; const kTestName = 'name'; const kXcodeProjecType = 'type'; final argParser = ArgParser() ..addOption(kDestination) ..addOption(kTestName) ..addOption(kXcodeProjecType); await task(() async { final ArgResults argResults = argParser.parse(args); final String? destination = argResults.option(kDestination); final Directory destinationDir; var destinationOverride = false; if (destination != null) { destinationOverride = true; destinationDir = Directory(destination); if (destinationDir.existsSync()) { destinationDir.deleteSync(recursive: true); } destinationDir.createSync(recursive: true); } else { destinationDir = Directory.systemTemp.createTempSync('flutter_module_test.'); } final String? testName = argResults.option(kTestName); XcodeProjectType? projectType; final String? projectTypeName = argResults.option(kXcodeProjecType); if (projectTypeName?.toLowerCase() == 'swiftui') { projectType = XcodeProjectType.SwiftUI; } else if (projectTypeName?.toLowerCase() == 'uikit-swift') { projectType = XcodeProjectType.UIKitSwift; } List projectTypesToTest = XcodeProjectType.values; if (projectType != null) { projectTypesToTest = [projectType]; } String? simulatorDeviceId; final templatesDir = Directory( path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_add2app_uiscene'), ); try { final Directory appDir = await _createFlutterModuleApp( destinationDir: destinationDir, templatesDir: templatesDir, ); final Directory pluginDir = await _createFlutterPlugin( destinationDir: destinationDir, templatesDir: templatesDir, ); var testCount = 0; var testFailedCount = 0; await testWithNewIOSSimulator( 'TestAdd2AppSim', deviceTypeId: 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-3rd-generation', (String deviceId) async { for (final xcodeProjectType in projectTypesToTest) { final (String xcodeProjectName, Directory xcodeProjectDir) = await _createNativeApp( destinationDir: destinationDir, templatesDir: templatesDir, xcodeProjectType: xcodeProjectType, ); simulatorDeviceId = deviceId; final scenarios = Scenarios(); final Map> scenariosMap = scenarios.scenarios( xcodeProjectType, ); for (final String scenarioName in scenariosMap.keys) { if (testName != null && scenarioName != testName) { continue; } final List replacements = FileReplacements.fromScenario( scenariosMap[scenarioName]!, templatesDir: templatesDir, xcodeProjectDir: xcodeProjectDir, pluginDir: pluginDir, appDir: appDir, ); for (final replacement in replacements) { replacement.replace(); } section('Test Scenario $scenarioName'); await _installPlugins(appDir: appDir, xcodeProjectDir: xcodeProjectDir); final int result = await _testNativeApp( deviceId: simulatorDeviceId!, scenarioName: scenarioName, templatesDir: templatesDir, xcodeProjectDir: xcodeProjectDir, xcodeProjectName: xcodeProjectName, ); testCount++; if (result != 0) { testFailedCount++; } // Reset files to original between scenarios unless we're targetting a specific test. if (testName == null) { for (final replacement in replacements) { replacement.reset(); } } } } }, ); if (testFailedCount > 0) { return TaskResult.failure( '$testFailedCount out of $testCount native tests failed. Search the logs for "** TEST FAILED **"', ); } return TaskResult.success(null); } catch (e, stackTrace) { print(e); print('Task exception stack trace:\n$stackTrace'); return TaskResult.failure(e.toString()); } finally { unawaited(removeIOSSimulator(simulatorDeviceId)); if (!destinationOverride) { rmTree(destinationDir); } } }); } Future _createFlutterModuleApp({ required Directory templatesDir, required Directory destinationDir, }) async { section('Create Flutter Module'); const moduleName = 'my_module'; await flutter( 'create', options: ['--org', 'dev.flutter.devicelab', '--template=module', moduleName], workingDirectory: destinationDir.path, ); return Directory(path.join(destinationDir.path, moduleName)); } Future _createFlutterPlugin({ required Directory templatesDir, required Directory destinationDir, }) async { section('Create Flutter Plugin'); const pluginName = 'my_plugin'; await flutter( 'create', options: [ '--org', 'dev.flutter.devicelab', '--template=plugin', '--platform=ios', pluginName, ], workingDirectory: destinationDir.path, ); return Directory(path.join(destinationDir.path, pluginName)); } Future<(String, Directory)> _createNativeApp({ required Directory templatesDir, required Directory destinationDir, required XcodeProjectType xcodeProjectType, }) async { section('Create Xcode Project'); final String xcodeProjectName; switch (xcodeProjectType) { case XcodeProjectType.UIKitSwift: xcodeProjectName = 'NativeUIKitSwiftExperiment'; case XcodeProjectType.SwiftUI: xcodeProjectName = 'NativeSwiftUIExperiment'; } // Copy Xcode project final xcodeProjectDir = Directory(path.join(destinationDir.path, xcodeProjectName)); xcodeProjectDir.createSync(recursive: true); final xcodeProjectTemplate = Directory(path.join(templatesDir.path, xcodeProjectName)); recursiveCopy(xcodeProjectTemplate, xcodeProjectDir); return (xcodeProjectName, xcodeProjectDir); } Future _installPlugins({ required Directory appDir, required Directory xcodeProjectDir, }) async { // Poke the pubspec to reset the fingerprinter to ensure the module is re-generated. // See [_regenerateModuleFromTemplateIfNeeded] in packages/flutter_tools/lib/src/xcode_project.dart. final pubspec = File(path.join(appDir.path, 'pubspec.yaml')); pubspec.writeAsStringSync(pubspec.readAsStringSync()); await flutter( 'build', options: ['ios', '--config-only', '-v'], workingDirectory: appDir.path, ); await flutter('pub', options: ['get'], workingDirectory: appDir.path); await eval( 'pod', ['install', '--verbose'], environment: { // See https://github.com/flutter/flutter/issues/10873. // CocoaPods analytics adds a lot of latency. 'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8', }, workingDirectory: xcodeProjectDir.path, ); } class FileReplacements { FileReplacements(this.templatePath, this.destinationPath); final String templatePath; final String destinationPath; String? originalContent; static List fromScenario( Map replacementMap, { required Directory templatesDir, required Directory xcodeProjectDir, required Directory pluginDir, required Directory appDir, }) { final replacements = []; for (final String source in replacementMap.keys) { final String destination = replacementMap[source]!; final String sourcePath = source.replaceFirst(r'$TEMPLATE_DIR', templatesDir.path); final String destinationPath = destination .replaceFirst(r'$XCODE_PROJ_DIR', xcodeProjectDir.path) .replaceFirst(r'$PLUGIN_DIR', pluginDir.path) .replaceFirst(r'$APP_DIR', appDir.path); replacements.add(FileReplacements(sourcePath, destinationPath)); } return replacements; } void replace() { final templateFile = File(templatePath); final destinationFile = File(destinationPath); if (!destinationFile.existsSync()) { File(destinationPath).createSync(recursive: true); } else { originalContent = destinationFile.readAsStringSync(); } templateFile.copySync(destinationPath); } void reset() { final destinationFile = File(destinationPath); if (originalContent != null) { destinationFile.writeAsStringSync(originalContent!); } else { if (destinationFile.existsSync()) { destinationFile.deleteSync(recursive: true); } } } } Future _testNativeApp({ required Directory templatesDir, required String scenarioName, required Directory xcodeProjectDir, required String deviceId, required String xcodeProjectName, }) async { final String resultBundleTemp = Directory.systemTemp .createTempSync('flutter_module_test_ios_xcresult.') .path; final String resultBundlePath = path.join(resultBundleTemp, 'result'); final int testResultExit = await exec( 'xcodebuild', [ '-workspace', '$xcodeProjectName.xcworkspace', '-scheme', xcodeProjectName, '-configuration', 'Debug', '-destination', 'id=$deviceId', '-resultBundlePath', resultBundlePath, 'test', '-parallel-testing-enabled', 'NO', 'COMPILER_INDEX_STORE_ENABLE=NO', ], workingDirectory: xcodeProjectDir.path, canFail: true, ); if (testResultExit != 0) { await _uploadTestResults(scenarioName: scenarioName, resultBundlePath: resultBundleTemp); } return testResultExit; } Future _uploadTestResults({ required String scenarioName, required String resultBundlePath, }) async { final Directory? dumpDirectory = hostAgent.dumpDirectory; if (dumpDirectory != null) { // Zip the test results to the artifacts directory for upload. final zipName = 'module_uiscene_test_ios-$scenarioName-${DateTime.now().toLocal().toIso8601String()}.zip'; await inDirectory(resultBundlePath, () { final String zipPath = path.join(dumpDirectory.path, zipName); return exec( 'zip', ['-r', '-9', '-q', zipPath, 'result.xcresult'], canFail: true, // Best effort to get the logs. ); }); } } enum XcodeProjectType { UIKitSwift, SwiftUI } class Scenarios { Scenarios(); Map> scenarios(XcodeProjectType projectType) { switch (projectType) { case XcodeProjectType.UIKitSwift: return uiKitSwiftScenarios; case XcodeProjectType.SwiftUI: return swiftUIScenarios; } } /// A map of scenario names to a map of file replacements. /// /// Each scenario is a different configuration for testing the Flutter module /// in a native iOS app. The file replacements are used to set up the /// specific configuration for each scenario. late Map> uiKitSwiftScenarios = >{ ...basicLifecycleScenarios, ...stateRestorationScenarios, ...implicitEngineDelegateScenarios, ...multiSceneScenarios, }; late Map> swiftUIScenarios = >{ 'SwiftUI-FlutterSceneDelegate': { ...sharedAppLifecycleFiles, ...sharedPluginLifecycleFiles, r'$TEMPLATE_DIR/native/Info-migrated-no-config.plist': r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/NativeSwiftUIExperiment-Info.plist', r'$TEMPLATE_DIR/native/SwiftUIApp-FlutterSceneDelegate.swift': r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/NativeSwiftUIExperimentApp.swift', r'$TEMPLATE_DIR/native/SwiftUIApp-ContentView.swift': r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/ContentView.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/UITests-SceneEvents-NoApplicationEvents.swift': r'$XCODE_PROJ_DIR/NativeSwiftUIExperimentUITests/NativeSwiftUIExperimentUITests.swift', }, 'SwiftUI-FlutterSceneLifeCycleProvider': { ...sharedAppLifecycleFiles, ...sharedPluginLifecycleFiles, r'$TEMPLATE_DIR/native/Info-migrated-no-config.plist': r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/NativeSwiftUIExperiment-Info.plist', r'$TEMPLATE_DIR/native/SwiftUIApp-FlutterSceneLifeCycleProvider.swift': r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/NativeSwiftUIExperimentApp.swift', r'$TEMPLATE_DIR/native/SwiftUIApp-ContentView.swift': r'$XCODE_PROJ_DIR/NativeSwiftUIExperiment/ContentView.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/UITests-SceneEvents-NoApplicationEvents.swift': r'$XCODE_PROJ_DIR/NativeSwiftUIExperimentUITests/NativeSwiftUIExperimentUITests.swift', }, }; late Map> basicLifecycleScenarios = >{ // When both the app and the plugin have migrated to scenes, we expect scene events. 'AppMigrated-FlutterSceneDelegate-PluginMigrated': { ...sharedLifecycleFiles, r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/UITests-SceneEvents.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, // When the app has migrated but the plugin hasn't, we expect application events to be used as // a fallback. 'AppMigrated-FlutterSceneDelegate-PluginNotMigrated': { ...sharedLifecycleFiles, r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-unmigrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/UITests-ApplicationEvents-AppMigrated.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, // When both the app and the plugin have migrated to scenes, we expect scene events. 'AppMigrated-FlutterSceneLifeCycleProvider-PluginMigrated': { ...sharedLifecycleFiles, r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneLifeCycleProvider.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/UITests-SceneEvents.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, // When the app has migrated but the plugin hasn't, we expect application events to be used as // a fallback. 'AppMigrated-FlutterSceneLifeCycleProvider-PluginNotMigrated': { ...sharedLifecycleFiles, r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneLifeCycleProvider.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-unmigrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/UITests-ApplicationEvents-AppMigrated.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, // When the app has not migrated, but the plugin supports both, we expect application events. 'AppNotMigrated-FlutterSceneDelegate-PluginMigrated': { ...sharedLifecycleFiles, r'$TEMPLATE_DIR/native/Info-unmigrated.plist': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/UITests-ApplicationEvents-AppNotMigrated.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, // When the app and plugin have not migrated, we expect application events. 'AppNotMigrated-FlutterSceneDelegate-PluginNotMigrated': { ...sharedLifecycleFiles, r'$TEMPLATE_DIR/native/Info-unmigrated.plist': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-unmigrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/UITests-ApplicationEvents-AppNotMigrated.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, }; late Map> multiSceneScenarios = >{ // When multi scene is enabled and the rootViewController is a FlutterViewController, we // expect all scene events without manual registration. 'MultiSceneEnabled-FlutterSceneDelegate-RootViewController': { ...sharedAppLifecycleFiles, ...sharedPluginLifecycleFiles, r'$TEMPLATE_DIR/native/Info-MultiSceneEnabled-Storyboard.plist': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', r'$TEMPLATE_DIR/native/AppDelegate-FlutterAppDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', r'$TEMPLATE_DIR/native/Main-FlutterViewController.storyboard': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Base.lproj/Main.storyboard', r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/UITests-SceneEvents.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, // When multi scene is enabled and the ViewController is created programatically with a // manually registered FlutterEngine, we expect all scene events. 'MultiSceneEnabled-FlutterSceneDelegate-ManualRegistration-NoStoryboard': { ...sharedAppLifecycleFiles, ...sharedPluginLifecycleFiles, r'$TEMPLATE_DIR/native/Info-MultiSceneEnabled-NoStoryboard.plist': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', r'$TEMPLATE_DIR/native/AppDelegate-FlutterAppDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate-MultiScene-NoStoryboard.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/native/ViewController-FlutterEngineFromSceneDelegate-NoStoryboard.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/ViewController.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/UITests-SceneEvents-NoApplicationEvents.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, // When multi scene is enabled and the ViewController is created via Storyboard with a // manually registered FlutterEngine, we expect all scene events. 'MultiSceneEnabled-FlutterSceneDelegate-ManualRegistration-Storyboard': { ...sharedAppLifecycleFiles, ...sharedPluginLifecycleFiles, r'$TEMPLATE_DIR/native/Info-MultiSceneEnabled-Storyboard.plist': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', r'$TEMPLATE_DIR/native/AppDelegate-FlutterAppDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate-MultiScene-Storyboard.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/native/ViewController-FlutterEngineFromSceneDelegate-Storyboard.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/ViewController.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/UITests-SceneEvents-NoApplicationEvents.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, }; late Map> stateRestorationScenarios = >{ // State restoration work both when migrated and when not. 'AppMigrated-StateRestoration': {...sharedStateRestorationFiles}, 'AppNotMigrated-StateRestoration': { ...sharedStateRestorationFiles, r'$TEMPLATE_DIR/native/Info-unmigrated.plist': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', }, }; late Map> implicitEngineDelegateScenarios = >{ // When using an implicit FlutterEngine created by the storyboard, we expect plugins to // receive application launch events and scene events. 'FlutterImplicitEngineDelegate-AppMigrated-StoryboardFlutterViewController': { ...sharedAppLifecycleFiles, ...sharedPluginLifecycleFiles, r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/Main-FlutterViewController.storyboard': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Base.lproj/Main.storyboard', r'$TEMPLATE_DIR/native/AppDelegate-FlutterImplicitEngineDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', r'$TEMPLATE_DIR/native/UITests-SceneEvents-ApplicationLaunchEvents.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, // When registering plugins with the AppDelegate's self (and therefore the FlutterLaunchEngine) // alongside the FlutterImplicitEngineDelegate, we expect application events starting where // registration occurs, such as `application:didFinishingLaunchingWithOptions`. 'FlutterImplicitEngineDelegateWithLaunchEngine-AppMigrated-StoryboardFlutterViewController': { ...sharedAppLifecycleFiles, ...sharedPluginLifecycleFiles, r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/Main-FlutterViewController.storyboard': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Base.lproj/Main.storyboard', r'$TEMPLATE_DIR/native/AppDelegate-FlutterImplicitEngineDelegateWithLaunchEngine.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', r'$TEMPLATE_DIR/native/UITests-SceneEvents.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, // When the app has not migrated to scenes, storyboard is instantiated earlier in the lifecycle. // So when using an implicit FlutterEngine created by the storyboard, we expect plugins to // receive all application events. 'FlutterImplicitEngineDelegate-AppNotMigrated-StoryboardFlutterViewController': { ...sharedAppLifecycleFiles, ...sharedPluginLifecycleFiles, r'$TEMPLATE_DIR/native/Info-unmigrated.plist': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Info.plist', r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/AppDelegate-FlutterImplicitEngineDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', r'$TEMPLATE_DIR/native/Main-FlutterViewController.storyboard': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Base.lproj/Main.storyboard', r'$TEMPLATE_DIR/native/UITests-ApplicationEvents-FlutterImplicitEngineDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, // When using an implicit FlutterEngine, created by the FlutterViewController in another // ViewController, we expect plugins to be registered after the FlutterViewController is // created, which results in the `application:didFinishLaunchingWithOptions:` and // `scene:willConnectToSession:options:` events being missed. This is not a expected use case // but it could be utilized. 'FlutterImplicitEngineDelegate-AppMigrated-ImplicitFlutterEngine': { ...sharedAppLifecycleFiles, ...sharedPluginLifecycleFiles, r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/flutterplugin/ios/LifecyclePlugin-migrated.swift': r'$PLUGIN_DIR/ios/Classes/MyPlugin.swift', r'$TEMPLATE_DIR/native/AppDelegate-FlutterImplicitEngineDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', r'$TEMPLATE_DIR/native/ViewController-ImplicitFlutterEngine.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/ViewController.swift', r'$TEMPLATE_DIR/native/UITests-SceneEventsNoConnect-NoApplicationEvents.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }, }; late Map sharedLifecycleFiles = { ...sharedAppLifecycleFiles, ...sharedPluginLifecycleFiles, r'$TEMPLATE_DIR/native/AppDelegate-FlutterAppDelegate-FlutterEngine.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', r'$TEMPLATE_DIR/native/ViewController-FlutterEngineFromAppDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/ViewController.swift', }; late Map sharedAppLifecycleFiles = { r'$TEMPLATE_DIR/flutterapp/lib/main-LifeCycleTest': r'$APP_DIR/lib/main.dart', r'$TEMPLATE_DIR/flutterapp/pubspec-LifeCycleTest.yaml': r'$APP_DIR/pubspec.yaml', }; late Map sharedPluginLifecycleFiles = { r'$TEMPLATE_DIR/flutterplugin/lib/lifecycle_plugin': r'$PLUGIN_DIR/lib/my_plugin.dart', r'$TEMPLATE_DIR/flutterplugin/lib/lifecycle_plugin_method_channel': r'$PLUGIN_DIR/lib/my_plugin_method_channel.dart', r'$TEMPLATE_DIR/flutterplugin/lib/lifecycle_plugin_platform_interface': r'$PLUGIN_DIR/lib/my_plugin_platform_interface.dart', }; late Map sharedStateRestorationFiles = { r'$TEMPLATE_DIR/flutterapp/lib/main-StateRestorationTest': r'$APP_DIR/lib/main.dart', r'$TEMPLATE_DIR/native/AppDelegate-FlutterAppDelegate-FlutterEngine.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/AppDelegate.swift', r'$TEMPLATE_DIR/native/SceneDelegate-FlutterSceneDelegate.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/SceneDelegate.swift', r'$TEMPLATE_DIR/native/Main-FlutterViewController-RestorationId.storyboard': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperiment/Base.lproj/Main.storyboard', r'$TEMPLATE_DIR/native/UITests-StateRestoration.swift': r'$XCODE_PROJ_DIR/NativeUIKitSwiftExperimentUITests/NativeUIKitSwiftExperimentUITests.swift', }; }