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]; } }