diff --git a/packages/quick_actions/quick_actions_ios/CHANGELOG.md b/packages/quick_actions/quick_actions_ios/CHANGELOG.md index 506580fd211c..4f1cbc7cf8eb 100644 --- a/packages/quick_actions/quick_actions_ios/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.2.4 + +* Adds support for UIScene lifecycle. +* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. + ## 1.2.3 * Updates to Pigeon 26. diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/quick_actions/quick_actions_ios/example/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7652ba..391a902b2beb 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/quick_actions/quick_actions_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift b/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift index 6fa401143d5a..b5bc5f9682ba 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift @@ -6,14 +6,17 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) super.application(application, didFinishLaunchingWithOptions: launchOptions) // For UI integration tests. See https://github.com/flutter/plugins/pull/3811. return false } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist index fd3b62987824..7fba3010b0d8 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist @@ -45,5 +45,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift index 98766194b3bc..e52d6a0a30d2 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift @@ -223,4 +223,110 @@ struct QuickActionsPluginTests { plugin.applicationDidBecomeActive(UIApplication.shared) } } + + // MARK: - Scene lifecycle tests + + @Test func windowScenePerformActionForShortcutItem() async { + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + await confirmation("shortcut should be handled via windowScene") { confirmed in + flutterApi.launchActionCallback = { aString in + #expect(aString == item.type) + confirmed() + } + + let windowScene = UIApplication.shared.connectedScenes.first as! UIWindowScene + let actionResult = plugin.windowScene( + windowScene, + performActionFor: item + ) { success in + } + + #expect(actionResult, "windowScene performActionFor must return true.") + } + } + + @Test func sceneWillConnectToWithoutShortcut() { + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + let connectResult = plugin.scene( + UIApplication.shared.connectedScenes.first!, + willConnectTo: UIApplication.shared.connectedScenes.first!.session, + options: nil) + #expect( + !connectResult, + "scene willConnectTo must return false if not launched from shortcut.") + } + + @Test func sceneDidBecomeActiveLaunchWithoutShortcut() async { + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + let connectResult = plugin.scene( + UIApplication.shared.connectedScenes.first!, + willConnectTo: UIApplication.shared.connectedScenes.first!.session, + options: nil) + #expect(!connectResult) + + await confirmation("launchAction should not be called", expectedCount: 0) { confirmed in + flutterApi.launchActionCallback = { _ in + confirmed() + } + plugin.sceneDidBecomeActive(UIApplication.shared.connectedScenes.first!) + } + } + + @Test func sceneDidBecomeActiveLaunchWithShortcut() async { + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + await confirmation("shortcut should be handled when scene becomes active") { confirmed in + flutterApi.launchActionCallback = { aString in + #expect(aString == item.type) + confirmed() + } + + // Simulate cold start: scene connects with a shortcut item via connectionOptions. + // We can't construct UIScene.ConnectionOptions directly, so we simulate the effect + // by calling the AppDelegate-style method that sets launchingShortcutType. + let launchResult = plugin.application( + UIApplication.shared, + didFinishLaunchingWithOptions: [UIApplication.LaunchOptionsKey.shortcutItem: item]) + #expect(!launchResult) + + plugin.sceneDidBecomeActive(UIApplication.shared.connectedScenes.first!) + } + } } diff --git a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift index 3f2d91518a11..2dec50fbfe97 100644 --- a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift +++ b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift @@ -3,8 +3,11 @@ // found in the LICENSE file. import Flutter +import UIKit -public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsApi { +public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsApi, + FlutterSceneLifeCycleDelegate +{ public static func register(with registrar: FlutterPluginRegistrar) { let messenger = registrar.messenger() @@ -12,6 +15,7 @@ public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsA let instance = QuickActionsPlugin(flutterApi: flutterApi) IOSQuickActionsApiSetup.setUp(binaryMessenger: messenger, api: instance) registrar.addApplicationDelegate(instance) + registrar.addSceneDelegate(instance) } private let shortcutItemProvider: ShortcutItemProviding @@ -72,6 +76,44 @@ public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsA } } + // MARK: - FlutterSceneLifeCycleDelegate + + public func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions? + ) -> Bool { + // Handle the case where app is launched via a shortcut item in scene-based lifecycle. + if let shortcutItem = connectionOptions?.shortcutItem { + // Keep hold of the shortcut type and handle it in the + // `sceneDidBecomeActive:` method once the Dart MethodChannel + // is initialized. + launchingShortcutType = shortcutItem.type + // Return true to indicate we handled the connection. + return true + } + return false + } + + public func sceneDidBecomeActive(_ scene: UIScene) { + if let shortcutType = launchingShortcutType { + handleShortcut(shortcutType) + launchingShortcutType = nil + } + } + + public func windowScene( + _ windowScene: UIWindowScene, + performActionFor shortcutItem: UIApplicationShortcutItem, + completionHandler: @escaping (Bool) -> Void + ) -> Bool { + handleShortcut(shortcutItem.type) + completionHandler(true) + return true + } + + // MARK: - Shortcut handling + func handleShortcut(_ shortcut: String) { flutterApi.launchAction(action: shortcut) { _ in // noop diff --git a/packages/quick_actions/quick_actions_ios/pubspec.yaml b/packages/quick_actions/quick_actions_ios/pubspec.yaml index 478edffea6ea..e1f48024ccf0 100644 --- a/packages/quick_actions/quick_actions_ios/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: quick_actions_ios description: An implementation for the iOS platform of the Flutter `quick_actions` plugin. repository: https://github.com/flutter/packages/tree/main/packages/quick_actions/quick_actions_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 1.2.3 +version: 1.2.4 environment: - sdk: ^3.9.0 - flutter: ">=3.35.0" + sdk: ^3.10.0 + flutter: ">=3.38.0" flutter: plugin: