Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/quick_actions/quick_actions_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,26 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ class MockFlutterApi: IOSQuickActionsFlutterApiProtocol {
}
}

class FakeConnectionOptions: UIScene.ConnectionOptions {
var _shortcutItem: UIApplicationShortcutItem?

override var shortcutItem: UIApplicationShortcutItem? {
return _shortcutItem
}

/// Creates a `FakeConnectionOptions` with the given shortcut item.
///
/// `UIScene.ConnectionOptions` has no accessible initializer in Swift, so
/// we go through `NSObject.Type.init()` (ObjC runtime) to allocate the subclass.
static func withShortcutItem(_ item: UIApplicationShortcutItem) -> FakeConnectionOptions {
let instance = (self as NSObject.Type).init() as! FakeConnectionOptions
instance._shortcutItem = item
return instance
}
}

@MainActor
struct QuickActionsPluginTests {

Expand Down Expand Up @@ -223,4 +241,111 @@ 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()
}

let options = FakeConnectionOptions.withShortcutItem(item)

let scene = UIApplication.shared.connectedScenes.first!
let connectResult = plugin.scene(
scene,
willConnectTo: scene.session,
options: options)
#expect(connectResult, "scene willConnectTo must return true when shortcut is provided.")

plugin.sceneDidBecomeActive(scene)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
// 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()
let flutterApi = IOSQuickActionsFlutterApi(binaryMessenger: messenger)
let instance = QuickActionsPlugin(flutterApi: flutterApi)
IOSQuickActionsApiSetup.setUp(binaryMessenger: messenger, api: instance)
registrar.addApplicationDelegate(instance)
registrar.addSceneDelegate(instance)
}

private let shortcutItemProvider: ShortcutItemProviding
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/quick_actions/quick_actions_ios/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading