Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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 @@ -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,10 @@ class MockFlutterApi: IOSQuickActionsFlutterApiProtocol {
}
}

struct MockConnectionOptions: ConnectionOptionsProtocol {
let shortcutItem: UIApplicationShortcutItem?
}

@MainActor
struct QuickActionsPluginTests {

Expand Down Expand Up @@ -223,4 +227,109 @@ 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
var completionSuccess: Bool?
let actionResult = plugin.windowScene(
windowScene,
performActionFor: item
) { success in
completionSuccess = success
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this closure called synchronously? (is completionSuccess assigned before #expect check below?)

}

#expect(actionResult, "windowScene performActionFor must return true.")
#expect(completionSuccess == 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 connectResult = plugin.handleSceneWillConnectTo(
connectionOptions: MockConnectionOptions(shortcutItem: item))
#expect(connectResult, "scene willConnectTo must return true when shortcut is provided.")

plugin.sceneDidBecomeActive(UIApplication.shared.connectedScenes.first!)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@
// found in the LICENSE file.

import Flutter
import UIKit

public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsApi {
protocol ConnectionOptionsProtocol {
var shortcutItem: UIApplicationShortcutItem? { get }
}

extension UIScene.ConnectionOptions: ConnectionOptionsProtocol {}

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 +82,48 @@ public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsA
}
}

// MARK: - FlutterSceneLifeCycleDelegate

public func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions?
) -> Bool {
return handleSceneWillConnectTo(connectionOptions: connectionOptions)
}

func handleSceneWillConnectTo(connectionOptions: ConnectionOptionsProtocol?) -> Bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is shortcutItem the only property needed in ConnectionOptions? If so, you can just pass in the shortcutItem here, and avoid writing the protocol.

// Handle the case where app is launched via a shortcut item in scene-based lifecycle.
if let shortcutItem = connectionOptions?.shortcutItem {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this new logic? did we have similar logic in legacy app delegate methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not new but similar to legacy which uses launchOptions[.shortcutItem]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to remove the legacy code?

// 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