From 80496d7662acea2bdeef601035b3553adf540fd2 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Wed, 1 Oct 2025 09:24:37 -0500 Subject: [PATCH] Add SwiftUI support for UIScene migration (#176230) When embedding Flutter in a SwiftUI app, it appears that SwiftUI wraps the scene delegate given by the developer in an internal class called `SwiftUI.AppSceneDelegate`. This causes the scene delegate to fail to check if it conforms to the `FlutterSceneLifeCycleProvider` protocol - even though it does. However, after force casting it, selectors respond and can be used. ## 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. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../bin/tasks/module_uiscene_test_ios.dart | 151 +++-- .../native/Info-migrated-no-config.plist | 13 + .../native/SwiftUIApp-ContentView.swift | 29 + .../SwiftUIApp-FlutterSceneDelegate.swift | 46 ++ ...tUIApp-FlutterSceneLifeCycleProvider.swift | 113 ++++ ...ests-SceneEvents-NoApplicationEvents.swift | 55 ++ .../ios_add2app_uiscene/xcode_swiftui/Podfile | 22 + .../xcode_swiftui/xcode-swiftui-Info.plist | 5 + .../xcode_swiftui.xcodeproj/project.pbxproj | 553 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../contents.xcworkspacedata | 10 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../Assets.xcassets/Contents.json | 6 + .../xcode_swiftui/ContentView.swift | 21 + .../xcode_swiftui/xcode_swiftuiApp.swift | 14 + .../ios/framework/Source/FlutterEngine.mm | 8 +- .../framework/Source/FlutterSceneLifeCycle.mm | 31 +- .../Source/FlutterSceneLifeCycleTest.mm | 30 + .../Source/FlutterSceneLifeCycle_Internal.h | 2 + .../ios/framework/Source/FlutterView.mm | 26 +- 21 files changed, 1119 insertions(+), 69 deletions(-) create mode 100644 dev/integration_tests/ios_add2app_uiscene/native/Info-migrated-no-config.plist create mode 100644 dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-ContentView.swift create mode 100644 dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-FlutterSceneDelegate.swift create mode 100644 dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-FlutterSceneLifeCycleProvider.swift create mode 100644 dev/integration_tests/ios_add2app_uiscene/native/UITests-SceneEvents-NoApplicationEvents.swift create mode 100644 dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/Podfile create mode 100644 dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode-swiftui-Info.plist create mode 100644 dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcodeproj/project.pbxproj create mode 100644 dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcworkspace/contents.xcworkspacedata create mode 100644 dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/Contents.json create mode 100644 dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/ContentView.swift create mode 100644 dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/xcode_swiftuiApp.swift diff --git a/dev/devicelab/bin/tasks/module_uiscene_test_ios.dart b/dev/devicelab/bin/tasks/module_uiscene_test_ios.dart index 995d65d804a..51c03c98904 100644 --- a/dev/devicelab/bin/tasks/module_uiscene_test_ios.dart +++ b/dev/devicelab/bin/tasks/module_uiscene_test_ios.dart @@ -27,9 +27,11 @@ import 'package:path/path.dart' as path; Future main(List args) async { const String kDestination = 'destination'; const String kTestName = 'name'; + const String kXcodeProjecType = 'type'; final ArgParser argParser = ArgParser() ..addOption(kDestination) - ..addOption(kTestName); + ..addOption(kTestName) + ..addOption(kXcodeProjecType); await task(() async { final ArgResults argResults = argParser.parse(args); @@ -49,6 +51,18 @@ Future main(List args) async { 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 Directory templatesDir = Directory( path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_add2app_uiscene'), @@ -63,50 +77,56 @@ Future main(List args) async { destinationDir: destinationDir, templatesDir: templatesDir, ); - final Directory xcodeProjectDir = await _createNativeApp( - destinationDir: destinationDir, - templatesDir: templatesDir, - xcodeProjectType: XcodeProjectType.UIKitSwift, - ); bool testFailed = false; - await testWithNewIOSSimulator('TestAdd2AppSim', (String deviceId) async { - simulatorDeviceId = deviceId; - final Scenarios scenarios = Scenarios(); - for (final String scenarioName in scenarios.scenarios.keys) { - if (testName != null && scenarioName != testName) { - continue; - } - final List replacements = FileReplacements.fromScenario( - scenarios.scenarios[scenarioName]!, + for (final XcodeProjectType xcodeProjectType in projectTypesToTest) { + final (String xcodeProjectName, Directory xcodeProjectDir) = await _createNativeApp( + destinationDir: destinationDir, templatesDir: templatesDir, - xcodeProjectDir: xcodeProjectDir, - pluginDir: pluginDir, - appDir: appDir, + xcodeProjectType: xcodeProjectType, ); - for (final FileReplacements 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, + simulatorDeviceId = deviceId; + final Scenarios scenarios = Scenarios(); + final Map> scenariosMap = scenarios.scenarios( + xcodeProjectType, ); - if (result != 0) { - testFailed = true; - } + 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, + ); - // Reset files to original between scenarios unless we're targetting a specific test. - if (testName == null) { for (final FileReplacements replacement in replacements) { - replacement.reset(); + 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, + ); + if (result != 0) { + testFailed = true; + } + + // Reset files to original between scenarios unless we're targetting a specific test. + if (testName == null) { + for (final FileReplacements replacement in replacements) { + replacement.reset(); + } } } } @@ -167,7 +187,7 @@ Future _createFlutterPlugin({ return Directory(path.join(destinationDir.path, pluginName)); } -Future _createNativeApp({ +Future<(String, Directory)> _createNativeApp({ required Directory templatesDir, required Directory destinationDir, required XcodeProjectType xcodeProjectType, @@ -179,12 +199,8 @@ Future _createNativeApp({ switch (xcodeProjectType) { case XcodeProjectType.UIKitSwift: xcodeProjectName = 'xcode_uikit_swift'; - case XcodeProjectType.UIKitObjC: - // TODO(vashworth): add Objective C integration test - throw UnimplementedError(); case XcodeProjectType.SwiftUI: - // TODO(vashworth): add SwiftUI integration test - throw UnimplementedError(); + xcodeProjectName = 'xcode_swiftui'; } // Copy Xcode project final Directory xcodeProjectDir = Directory(path.join(destinationDir.path, xcodeProjectName)); @@ -192,7 +208,7 @@ Future _createNativeApp({ final Directory xcodeProjectTemplate = Directory(path.join(templatesDir.path, xcodeProjectName)); recursiveCopy(xcodeProjectTemplate, xcodeProjectDir); - return xcodeProjectDir; + return (xcodeProjectName, xcodeProjectDir); } Future _installPlugins({ @@ -279,6 +295,7 @@ Future _testNativeApp({ required String scenarioName, required Directory xcodeProjectDir, required String deviceId, + required String xcodeProjectName, }) async { final String resultBundleTemp = Directory.systemTemp .createTempSync('flutter_module_test_ios_xcresult.') @@ -288,9 +305,9 @@ Future _testNativeApp({ 'xcodebuild', [ '-workspace', - 'xcode_uikit_swift.xcworkspace', + '$xcodeProjectName.xcworkspace', '-scheme', - 'xcode_uikit_swift', + xcodeProjectName, '-configuration', 'Debug', '-destination', @@ -332,17 +349,26 @@ Future _uploadTestResults({ } } -enum XcodeProjectType { UIKitSwift, UIKitObjC, SwiftUI } +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> scenarios = >{ + late Map> uiKitSwiftScenarios = >{ // When both the app and the plugin have migrated to scenes, we expect scene events. 'AppMigrated-FlutterSceneDelegate-PluginMigrated': { ...sharedLifecycleFiles, @@ -412,6 +438,37 @@ class Scenarios { }, }; + late Map> swiftUIScenarios = >{ + 'SwiftUI-FlutterSceneDelegate': { + ...sharedAppLifecycleFiles, + ...sharedPluginLifecycleFiles, + r'$TEMPLATE_DIR/native/Info-migrated-no-config.plist': + r'$XCODE_PROJ_DIR/xcode_swiftui/xcode_swiftui-Info.plist', + r'$TEMPLATE_DIR/native/SwiftUIApp-FlutterSceneDelegate.swift': + r'$XCODE_PROJ_DIR/xcode_swiftui/xcode_swiftuiApp.swift', + r'$TEMPLATE_DIR/native/SwiftUIApp-ContentView.swift': + r'$XCODE_PROJ_DIR/xcode_swiftui/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/xcode_swiftuiUITests/xcode_swiftuiUITests.swift', + }, + 'SwiftUI-FlutterSceneLifeCycleProvider': { + ...sharedAppLifecycleFiles, + ...sharedPluginLifecycleFiles, + r'$TEMPLATE_DIR/native/Info-migrated-no-config.plist': + r'$XCODE_PROJ_DIR/xcode_swiftui/xcode_swiftui-Info.plist', + r'$TEMPLATE_DIR/native/SwiftUIApp-FlutterSceneLifeCycleProvider.swift': + r'$XCODE_PROJ_DIR/xcode_swiftui/xcode_swiftuiApp.swift', + r'$TEMPLATE_DIR/native/SwiftUIApp-ContentView.swift': + r'$XCODE_PROJ_DIR/xcode_swiftui/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/xcode_swiftuiUITests/xcode_swiftuiUITests.swift', + }, + }; + late Map sharedLifecycleFiles = { ...sharedAppLifecycleFiles, ...sharedPluginLifecycleFiles, diff --git a/dev/integration_tests/ios_add2app_uiscene/native/Info-migrated-no-config.plist b/dev/integration_tests/ios_add2app_uiscene/native/Info-migrated-no-config.plist new file mode 100644 index 00000000000..816a1493708 --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/native/Info-migrated-no-config.plist @@ -0,0 +1,13 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + + + diff --git a/dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-ContentView.swift b/dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-ContentView.swift new file mode 100644 index 00000000000..e0d83a39f21 --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-ContentView.swift @@ -0,0 +1,29 @@ +// 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 Flutter +import SwiftUI + +struct FlutterViewControllerRepresentable: UIViewControllerRepresentable { + @Environment(AppDelegate.self) var appDelegate + + func makeUIViewController(context: Context) -> some UIViewController { + return FlutterViewController( + engine: appDelegate.flutterEngine, + nibName: nil, + bundle: nil + ) + } + + func updateUIViewController( + _ uiViewController: UIViewControllerType, + context: Context + ) {} +} + +struct ContentView: View { + var body: some View { + FlutterViewControllerRepresentable() + } +} diff --git a/dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-FlutterSceneDelegate.swift b/dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-FlutterSceneDelegate.swift new file mode 100644 index 00000000000..2e224e18593 --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-FlutterSceneDelegate.swift @@ -0,0 +1,46 @@ +// 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 Flutter +import FlutterPluginRegistrant +import SwiftUI + +@Observable +class AppDelegate: FlutterAppDelegate { + let flutterEngine = FlutterEngine(name: "my flutter engine") + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication + .LaunchOptionsKey: Any]? + ) -> Bool { + flutterEngine.run() + GeneratedPluginRegistrant.register(with: self.flutterEngine) + return true + } + + override func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + let configuration = UISceneConfiguration( + name: nil, + sessionRole: connectingSceneSession.role + ) + configuration.delegateClass = FlutterSceneDelegate.self + return configuration + } +} + +@main +struct xcode_swiftuiApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-FlutterSceneLifeCycleProvider.swift b/dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-FlutterSceneLifeCycleProvider.swift new file mode 100644 index 00000000000..6dabf06ed88 --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/native/SwiftUIApp-FlutterSceneLifeCycleProvider.swift @@ -0,0 +1,113 @@ +// 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 Flutter +import FlutterPluginRegistrant +import SwiftUI + +@Observable +class AppDelegate: FlutterAppDelegate { + let flutterEngine = FlutterEngine(name: "my flutter engine") + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication + .LaunchOptionsKey: Any]? + ) -> Bool { + flutterEngine.run() + GeneratedPluginRegistrant.register(with: self.flutterEngine) + return true + } + + override func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + let configuration = UISceneConfiguration( + name: nil, + sessionRole: connectingSceneSession.role + ) + configuration.delegateClass = SceneDelegate.self + return configuration + } +} + +class SceneDelegate: UIResponder, UIWindowSceneDelegate, + FlutterSceneLifeCycleProvider +{ + + var sceneLifeCycleDelegate: FlutterPluginSceneLifeCycleDelegate = + FlutterPluginSceneLifeCycleDelegate() + + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard (scene as? UIWindowScene) != nil else { return } + + sceneLifeCycleDelegate.scene( + scene, + willConnectTo: session, + options: connectionOptions + ) + } + + func sceneDidDisconnect(_ scene: UIScene) { + sceneLifeCycleDelegate.sceneDidDisconnect(scene) + } + + func sceneWillEnterForeground(_ scene: UIScene) { + sceneLifeCycleDelegate.sceneWillEnterForeground(scene) + } + + func sceneDidBecomeActive(_ scene: UIScene) { + sceneLifeCycleDelegate.sceneDidBecomeActive(scene) + } + + func sceneWillResignActive(_ scene: UIScene) { + sceneLifeCycleDelegate.sceneWillResignActive(scene) + } + + func sceneDidEnterBackground(_ scene: UIScene) { + sceneLifeCycleDelegate.sceneDidEnterBackground(scene) + } + + func scene( + _ scene: UIScene, + openURLContexts URLContexts: Set + ) { + sceneLifeCycleDelegate.scene(scene, openURLContexts: URLContexts) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + sceneLifeCycleDelegate.scene(scene, continue: userActivity) + } + + func windowScene( + _ windowScene: UIWindowScene, + performActionFor shortcutItem: UIApplicationShortcutItem, + completionHandler: @escaping (Bool) -> Void + ) { + sceneLifeCycleDelegate.windowScene( + windowScene, + performActionFor: shortcutItem, + completionHandler: completionHandler + ) + } +} + +@main +struct xcode_swiftuiApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/dev/integration_tests/ios_add2app_uiscene/native/UITests-SceneEvents-NoApplicationEvents.swift b/dev/integration_tests/ios_add2app_uiscene/native/UITests-SceneEvents-NoApplicationEvents.swift new file mode 100644 index 00000000000..1bd19653eaf --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/native/UITests-SceneEvents-NoApplicationEvents.swift @@ -0,0 +1,55 @@ +// 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 XCTest + +final class xcode_uikit_swiftUITests: XCTestCase { + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLifecycleEvents() throws { + let app = XCUIApplication() + app.launch() + let button = app.buttons["Get Lifecycle Events"].firstMatch + XCTAssertTrue(button.waitForExistence(timeout: 5)) + button.tap() + + let expectedStartEvents = [ + "sceneWillConnect", + "sceneWillEnterForeground", "sceneDidBecomeActive", + ] + let startEventsPredicate = NSPredicate( + format: "label == %@", + expectedStartEvents.joined(separator: "\n") + ) + let startEventsElement = app.staticTexts.element( + matching: startEventsPredicate + ) + XCTAssertTrue(startEventsElement.waitForExistence(timeout: 5)) + + // Background the app, then reactivate it and check the events again + XCUIDevice.shared.press(.home) + app.activate() + XCTAssertTrue(button.waitForExistence(timeout: 5)) + button.tap() + + let expectedEventsAfterBackgroundAndReactivate = [ + "sceneWillConnect", + "sceneWillEnterForeground", "sceneDidBecomeActive", + "sceneWillResignActive", "sceneDidEnterBackground", + "sceneWillEnterForeground", "sceneDidBecomeActive", + ] + let backgroundEventsPredicate = NSPredicate( + format: "label == %@", + expectedEventsAfterBackgroundAndReactivate.joined(separator: "\n") + ) + let backgroundEventsElement = app.staticTexts.element( + matching: backgroundEventsPredicate + ) + XCTAssertTrue(backgroundEventsElement.waitForExistence(timeout: 5)) + } +} diff --git a/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/Podfile b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/Podfile new file mode 100644 index 00000000000..d04cf9b1f84 --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/Podfile @@ -0,0 +1,22 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +flutter_application_path = '../my_module' +load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb') + +target 'xcode_swiftui' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for xcode_swiftui + install_all_flutter_pods(flutter_application_path) + + target 'xcode_swiftuiUITests' do + # Pods for testing + end + +end + +post_install do |installer| + flutter_post_install(installer) if defined?(flutter_post_install) +end diff --git a/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode-swiftui-Info.plist b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode-swiftui-Info.plist new file mode 100644 index 00000000000..0c67376ebac --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode-swiftui-Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcodeproj/project.pbxproj b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..657179a24e0 --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcodeproj/project.pbxproj @@ -0,0 +1,553 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 09AE28A9A100161970ABA956 /* Pods_xcode_swiftui.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 149DE053CEEA02EA47E518CD /* Pods_xcode_swiftui.framework */; }; + 1991B26C9F33F99FB17C6D71 /* Pods_xcode_swiftui_xcode_swiftuiUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A71D00ADA4BD1423A4888D5 /* Pods_xcode_swiftui_xcode_swiftuiUITests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 787D51772E8A1A61002EB011 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 787D51572E8A1A60002EB011 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 787D515E2E8A1A60002EB011; + remoteInfo = xcode_swiftui; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0625163ECFD280D6722E02EB /* Pods-xcode_swiftui-xcode_swiftuiUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-xcode_swiftui-xcode_swiftuiUITests.debug.xcconfig"; path = "Target Support Files/Pods-xcode_swiftui-xcode_swiftuiUITests/Pods-xcode_swiftui-xcode_swiftuiUITests.debug.xcconfig"; sourceTree = ""; }; + 149DE053CEEA02EA47E518CD /* Pods_xcode_swiftui.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_xcode_swiftui.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 36F75FFFDAB0368B5D9AEF50 /* Pods-xcode_swiftuiTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-xcode_swiftuiTests.release.xcconfig"; path = "Target Support Files/Pods-xcode_swiftuiTests/Pods-xcode_swiftuiTests.release.xcconfig"; sourceTree = ""; }; + 5B754AE90ACC1361E738A440 /* Pods-xcode_swiftui.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-xcode_swiftui.debug.xcconfig"; path = "Target Support Files/Pods-xcode_swiftui/Pods-xcode_swiftui.debug.xcconfig"; sourceTree = ""; }; + 714588AEC6F0B96B28798AB0 /* Pods-xcode_swiftui.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-xcode_swiftui.release.xcconfig"; path = "Target Support Files/Pods-xcode_swiftui/Pods-xcode_swiftui.release.xcconfig"; sourceTree = ""; }; + 787D515F2E8A1A60002EB011 /* xcode_swiftui.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = xcode_swiftui.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 787D51762E8A1A61002EB011 /* xcode_swiftuiUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = xcode_swiftuiUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 8A71D00ADA4BD1423A4888D5 /* Pods_xcode_swiftui_xcode_swiftuiUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_xcode_swiftui_xcode_swiftuiUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8B2B617A60DAC180929CD642 /* Pods-xcode_swiftuiTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-xcode_swiftuiTests.debug.xcconfig"; path = "Target Support Files/Pods-xcode_swiftuiTests/Pods-xcode_swiftuiTests.debug.xcconfig"; sourceTree = ""; }; + BB561EFA00EC91BA8A168BE3 /* Pods-xcode_swiftui-xcode_swiftuiUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-xcode_swiftui-xcode_swiftuiUITests.release.xcconfig"; path = "Target Support Files/Pods-xcode_swiftui-xcode_swiftuiUITests/Pods-xcode_swiftui-xcode_swiftuiUITests.release.xcconfig"; sourceTree = ""; }; + EE34DDFB6BB2E47ABD6F8EE5 /* Pods_xcode_swiftuiTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_xcode_swiftuiTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 787D51612E8A1A60002EB011 /* xcode_swiftui */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = xcode_swiftui; + sourceTree = ""; + }; + 787D51792E8A1A61002EB011 /* xcode_swiftuiUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = xcode_swiftuiUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 787D515C2E8A1A60002EB011 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 09AE28A9A100161970ABA956 /* Pods_xcode_swiftui.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 787D51732E8A1A61002EB011 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1991B26C9F33F99FB17C6D71 /* Pods_xcode_swiftui_xcode_swiftuiUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3FAB8759C7E0FBF58FE109A5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 149DE053CEEA02EA47E518CD /* Pods_xcode_swiftui.framework */, + 8A71D00ADA4BD1423A4888D5 /* Pods_xcode_swiftui_xcode_swiftuiUITests.framework */, + EE34DDFB6BB2E47ABD6F8EE5 /* Pods_xcode_swiftuiTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 787D51562E8A1A60002EB011 = { + isa = PBXGroup; + children = ( + 787D51612E8A1A60002EB011 /* xcode_swiftui */, + 787D51792E8A1A61002EB011 /* xcode_swiftuiUITests */, + 787D51602E8A1A60002EB011 /* Products */, + E283866923164D07FF92228F /* Pods */, + 3FAB8759C7E0FBF58FE109A5 /* Frameworks */, + ); + sourceTree = ""; + }; + 787D51602E8A1A60002EB011 /* Products */ = { + isa = PBXGroup; + children = ( + 787D515F2E8A1A60002EB011 /* xcode_swiftui.app */, + 787D51762E8A1A61002EB011 /* xcode_swiftuiUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + E283866923164D07FF92228F /* Pods */ = { + isa = PBXGroup; + children = ( + 5B754AE90ACC1361E738A440 /* Pods-xcode_swiftui.debug.xcconfig */, + 714588AEC6F0B96B28798AB0 /* Pods-xcode_swiftui.release.xcconfig */, + 0625163ECFD280D6722E02EB /* Pods-xcode_swiftui-xcode_swiftuiUITests.debug.xcconfig */, + BB561EFA00EC91BA8A168BE3 /* Pods-xcode_swiftui-xcode_swiftuiUITests.release.xcconfig */, + 8B2B617A60DAC180929CD642 /* Pods-xcode_swiftuiTests.debug.xcconfig */, + 36F75FFFDAB0368B5D9AEF50 /* Pods-xcode_swiftuiTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 787D515E2E8A1A60002EB011 /* xcode_swiftui */ = { + isa = PBXNativeTarget; + buildConfigurationList = 787D51802E8A1A61002EB011 /* Build configuration list for PBXNativeTarget "xcode_swiftui" */; + buildPhases = ( + 6E75E8B810F47E67163AA6AA /* [CP] Check Pods Manifest.lock */, + 787D515B2E8A1A60002EB011 /* Sources */, + 787D515C2E8A1A60002EB011 /* Frameworks */, + 787D515D2E8A1A60002EB011 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 787D51612E8A1A60002EB011 /* xcode_swiftui */, + ); + name = xcode_swiftui; + productName = xcode_swiftui; + productReference = 787D515F2E8A1A60002EB011 /* xcode_swiftui.app */; + productType = "com.apple.product-type.application"; + }; + 787D51752E8A1A61002EB011 /* xcode_swiftuiUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 787D51862E8A1A61002EB011 /* Build configuration list for PBXNativeTarget "xcode_swiftuiUITests" */; + buildPhases = ( + B6AB117EAEF0BDF8D52A82C6 /* [CP] Check Pods Manifest.lock */, + 787D51722E8A1A61002EB011 /* Sources */, + 787D51732E8A1A61002EB011 /* Frameworks */, + 787D51742E8A1A61002EB011 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 787D51782E8A1A61002EB011 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 787D51792E8A1A61002EB011 /* xcode_swiftuiUITests */, + ); + name = xcode_swiftuiUITests; + productName = xcode_swiftuiUITests; + productReference = 787D51762E8A1A61002EB011 /* xcode_swiftuiUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 787D51572E8A1A60002EB011 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2600; + LastUpgradeCheck = 2600; + TargetAttributes = { + 787D515E2E8A1A60002EB011 = { + CreatedOnToolsVersion = 26.0; + }; + 787D51752E8A1A61002EB011 = { + CreatedOnToolsVersion = 26.0; + TestTargetID = 787D515E2E8A1A60002EB011; + }; + }; + }; + buildConfigurationList = 787D515A2E8A1A60002EB011 /* Build configuration list for PBXProject "xcode_swiftui" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 787D51562E8A1A60002EB011; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 787D51602E8A1A60002EB011 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 787D515E2E8A1A60002EB011 /* xcode_swiftui */, + 787D51752E8A1A61002EB011 /* xcode_swiftuiUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 787D515D2E8A1A60002EB011 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 787D51742E8A1A61002EB011 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6E75E8B810F47E67163AA6AA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-xcode_swiftui-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + B6AB117EAEF0BDF8D52A82C6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-xcode_swiftui-xcode_swiftuiUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 787D515B2E8A1A60002EB011 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 787D51722E8A1A61002EB011 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 787D51782E8A1A61002EB011 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 787D515E2E8A1A60002EB011 /* xcode_swiftui */; + targetProxy = 787D51772E8A1A61002EB011 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 787D517E2E8A1A61002EB011 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 787D517F2E8A1A61002EB011 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 787D51812E8A1A61002EB011 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5B754AE90ACC1361E738A440 /* Pods-xcode_swiftui.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "xcode-swiftui-Info.plist"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "io.flutter.devicelab.xcode-swiftui"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 787D51822E8A1A61002EB011 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 714588AEC6F0B96B28798AB0 /* Pods-xcode_swiftui.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "xcode-swiftui-Info.plist"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "io.flutter.devicelab.xcode-swiftui"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 787D51872E8A1A61002EB011 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0625163ECFD280D6722E02EB /* Pods-xcode_swiftui-xcode_swiftuiUITests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = S8QB4VV633; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "io.flutter.devicelab.xcode-swiftuiUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = xcode_swiftui; + }; + name = Debug; + }; + 787D51882E8A1A61002EB011 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BB561EFA00EC91BA8A168BE3 /* Pods-xcode_swiftui-xcode_swiftuiUITests.release.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = S8QB4VV633; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "io.flutter.devicelab.xcode-swiftuiUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = xcode_swiftui; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 787D515A2E8A1A60002EB011 /* Build configuration list for PBXProject "xcode_swiftui" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 787D517E2E8A1A61002EB011 /* Debug */, + 787D517F2E8A1A61002EB011 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 787D51802E8A1A61002EB011 /* Build configuration list for PBXNativeTarget "xcode_swiftui" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 787D51812E8A1A61002EB011 /* Debug */, + 787D51822E8A1A61002EB011 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 787D51862E8A1A61002EB011 /* Build configuration list for PBXNativeTarget "xcode_swiftuiUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 787D51872E8A1A61002EB011 /* Debug */, + 787D51882E8A1A61002EB011 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 787D51572E8A1A60002EB011 /* Project object */; +} diff --git a/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000000..919434a6254 --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcworkspace/contents.xcworkspacedata b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000000..457d4d1cd78 --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/AccentColor.colorset/Contents.json b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/AppIcon.appiconset/Contents.json b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..2305880107d --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/Contents.json b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/ContentView.swift b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/ContentView.swift new file mode 100644 index 00000000000..1d87238b727 --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/ContentView.swift @@ -0,0 +1,21 @@ +// 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 SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/xcode_swiftuiApp.swift b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/xcode_swiftuiApp.swift new file mode 100644 index 00000000000..ea03864988d --- /dev/null +++ b/dev/integration_tests/ios_add2app_uiscene/xcode_swiftui/xcode_swiftui/xcode_swiftuiApp.swift @@ -0,0 +1,14 @@ +// 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 SwiftUI + +@main +struct xcode_swiftuiApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 895694c1c9f..a23d3b0f32d 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -284,10 +284,10 @@ static constexpr int kNumProfilerSamplesPerSec = 5; // plugins to receive the `scene:willConnectToSession:options` event. // If we want to support multi-window on iPad later, we may need to add a way for deveopers to // register their FlutterEngine to the scene manually during this event. - if ([scene.delegate conformsToProtocol:@protocol(FlutterSceneLifeCycleProvider)]) { - NSObject* sceneProvider = - (NSObject*)scene.delegate; - [sceneProvider.sceneLifeCycleDelegate engine:self receivedConnectNotificationFor:scene]; + FlutterPluginSceneLifeCycleDelegate* sceneLifeCycleDelegate = + [FlutterPluginSceneLifeCycleDelegate fromScene:scene]; + if (sceneLifeCycleDelegate != nil) { + return [sceneLifeCycleDelegate engine:self receivedConnectNotificationFor:scene]; } } } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle.mm index 47235584764..f9bc82e39a8 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle.mm @@ -252,6 +252,28 @@ FLUTTER_ASSERT_ARC } return consumedByPlugin; } + ++ (FlutterPluginSceneLifeCycleDelegate*)fromScene:(UIScene*)scene { + if ([scene.delegate conformsToProtocol:@protocol(FlutterSceneLifeCycleProvider)]) { + NSObject* sceneProvider = + (NSObject*)scene.delegate; + return sceneProvider.sceneLifeCycleDelegate; + } + + // When embedded in a SwiftUI app, the scene delegate does not conform to + // FlutterSceneLifeCycleProvider even if it does. However, after force casting it, + // selectors respond and can be used. + NSObject* sceneProvider = + (NSObject*)scene.delegate; + if ([sceneProvider respondsToSelector:@selector(sceneLifeCycleDelegate)]) { + id sceneLifeCycleDelegate = sceneProvider.sceneLifeCycleDelegate; + // Double check that the selector is the expected class. + if ([sceneLifeCycleDelegate isKindOfClass:[FlutterPluginSceneLifeCycleDelegate class]]) { + return (FlutterPluginSceneLifeCycleDelegate*)sceneLifeCycleDelegate; + } + } + return nil; +} @end @implementation FlutterEnginePluginSceneLifeCycleDelegate { @@ -286,11 +308,10 @@ FLUTTER_ASSERT_ARC for (NSObject* delegate in _delegates.allObjects) { if ([delegate respondsToSelector:_cmd]) { // If this event has already been consumed by a plugin, send the event with nil options. - if (consumedByPlugin) { - [delegate scene:scene willConnectToSession:session options:nil]; - continue; - } else if ([delegate scene:scene willConnectToSession:session options:connectionOptions]) { - // Only allow one plugin to process this event. + // Only allow one plugin to process the connection options. + if ([delegate scene:scene + willConnectToSession:session + options:(consumedByPlugin ? nil : connectionOptions)]) { consumedByPlugin = YES; } } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycleTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycleTest.mm index 3c27236a2b8..4c4ffb77622 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycleTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycleTest.mm @@ -5,6 +5,7 @@ #import #import #import +#include #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate_internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle_Internal.h" @@ -13,6 +14,24 @@ FLUTTER_ASSERT_ARC +// This class is meant to mimic the SceneDelegate class generated by SwiftUI that does not conform +// FlutterSceneLifeCycleProvider but actually does. +@interface FlutterSwiftUIAppSceneDelegate : NSObject +@property(nonatomic, strong) FlutterPluginSceneLifeCycleDelegate* sceneLifeCycleDelegate; +@end + +@implementation FlutterSwiftUIAppSceneDelegate + +@synthesize sceneLifeCycleDelegate = _sceneLifeCycleDelegate; + +- (instancetype)init { + if (self = [super init]) { + _sceneLifeCycleDelegate = [[FlutterPluginSceneLifeCycleDelegate alloc] init]; + } + return self; +} +@end + @interface FlutterSceneLifecycleTest : XCTestCase @end @@ -799,4 +818,15 @@ FLUTTER_ASSERT_ARC completionHandler:handler]); } +- (void)testFlutterPluginSceneLifeCycleDelegateFromScene { + id mockScene = OCMClassMock([UIWindowScene class]); + id mockSceneDelegate = OCMClassMock([FlutterSwiftUIAppSceneDelegate class]); + id mockSceneLifeCycleDelegate = OCMClassMock([FlutterPluginSceneLifeCycleDelegate class]); + + OCMStub([mockScene delegate]).andReturn(mockSceneDelegate); + OCMStub([mockSceneDelegate sceneLifeCycleDelegate]).andReturn(mockSceneLifeCycleDelegate); + + XCTAssertEqual([FlutterPluginSceneLifeCycleDelegate fromScene:mockScene], + mockSceneLifeCycleDelegate); +} @end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle_Internal.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle_Internal.h index e404d6eee61..c88a1be3607 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSceneLifeCycle_Internal.h @@ -20,6 +20,8 @@ - (void)engine:(FlutterEngine*)engine receivedConnectNotificationFor:(UIScene*)scene; ++ (FlutterPluginSceneLifeCycleDelegate*)fromScene:(UIScene*)scene; + @end /** diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm index 3a46e914439..8262b6733b2 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm @@ -250,21 +250,21 @@ static void PrintWideGamutWarningOnce() { if (newScene == previousScene) { return; } - if ([newScene.delegate conformsToProtocol:@protocol(FlutterSceneLifeCycleProvider)]) { - id lifeCycleProvider = - (id)newScene.delegate; - [lifeCycleProvider.sceneLifeCycleDelegate addFlutterEngine:(FlutterEngine*)self.delegate]; + FlutterPluginSceneLifeCycleDelegate* newSceneLifeCycleDelegate = + [FlutterPluginSceneLifeCycleDelegate fromScene:newScene]; + if (newSceneLifeCycleDelegate != nil) { + return [newSceneLifeCycleDelegate addFlutterEngine:(FlutterEngine*)self.delegate]; } - if ([previousScene.delegate conformsToProtocol:@protocol(FlutterSceneLifeCycleProvider)]) { - // The window, and therefore windowScene, property may be nil if the receiver does not currently - // reside in any window. This occurs when the receiver has just been removed from its superview - // or when the receiver has just been added to a superview that is not attached to a window. - // Remove the engine from the previous scene if set since it is no longer in that window and - // scene. - id lifeCycleProvider = - (id)previousScene.delegate; - [lifeCycleProvider.sceneLifeCycleDelegate removeFlutterEngine:(FlutterEngine*)self.delegate]; + // The window, and therefore windowScene, property may be nil if the receiver does not currently + // reside in any window. This occurs when the receiver has just been removed from its superview + // or when the receiver has just been added to a superview that is not attached to a window. + // Remove the engine from the previous scene if set since it is no longer in that window and + // scene. + FlutterPluginSceneLifeCycleDelegate* previousSceneLifeCycleDelegate = + [FlutterPluginSceneLifeCycleDelegate fromScene:previousScene]; + if (previousSceneLifeCycleDelegate != nil) { + return [previousSceneLifeCycleDelegate removeFlutterEngine:(FlutterEngine*)self.delegate]; } }