Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[quick_actions] handle cold start on iOS correctly #3811

Merged
merged 4 commits into from
Apr 19, 2021
Merged
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
4 changes: 4 additions & 0 deletions packages/quick_actions/quick_actions/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.0+1

* Correctly handle iOS Application lifecycle events on cold start of the App.

## 0.6.0

* Migrate to federated architecture.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ - (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
[super application:application didFinishLaunchingWithOptions:launchOptions];
return NO;
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,45 @@ - (void)setUp {
self.continueAfterFailure = NO;
}

- (void)testQuickAction {
- (void)testQuickActionWithFreshStart {
XCUIApplication *app = [[XCUIApplication alloc] init];
[app launch];
[app terminate];

XCUIApplication *springboard =
[[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"];
XCUIElement *quickActionsAppIcon = springboard.icons[@"quick_actions_example"];
if (![quickActionsAppIcon waitForExistenceWithTimeout:kElementWaitingTime]) {
os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription);
XCTFail(@"Failed due to not able to find the example app from springboard with %@ seconds",
@(kElementWaitingTime));
}

[quickActionsAppIcon pressForDuration:2];
XCUIElement *actionTwo = springboard.buttons[@"Action two"];
if (![actionTwo waitForExistenceWithTimeout:kElementWaitingTime]) {
os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription);
XCTFail(@"Failed due to not able to find the actionTwo button from springboard with %@ seconds",
@(kElementWaitingTime));
}

[actionTwo tap];

XCUIElement *actionTwoConfirmation = app.otherElements[@"action_two"];
if (![actionTwoConfirmation waitForExistenceWithTimeout:kElementWaitingTime]) {
os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription);
XCTFail(@"Failed due to not able to find the actionTwoConfirmation in the app with %@ seconds",
@(kElementWaitingTime));
}
XCTAssertTrue(actionTwoConfirmation.exists);

[app terminate];
}

- (void)testQuickActionWhenAppIsInBackground {
XCUIApplication *app = [[XCUIApplication alloc] init];
[app launch];

XCUIElement *actionsReady = app.otherElements[@"actions ready"];
if (![actionsReady waitForExistenceWithTimeout:kElementWaitingTime]) {
os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription);
Expand Down Expand Up @@ -56,6 +92,8 @@ - (void)testQuickAction {
@(kElementWaitingTime));
}
XCTAssertTrue(actionOneConfirmation.exists);

[app terminate];
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

@interface FLTQuickActionsPlugin ()
@property(nonatomic, retain) FlutterMethodChannel *channel;
@property(nonatomic, retain) NSString *shortcutType;
@end

@implementation FLTQuickActionsPlugin
Expand Down Expand Up @@ -50,12 +51,44 @@ - (BOOL)application:(UIApplication *)application
performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem
completionHandler:(void (^)(BOOL succeeded))completionHandler
API_AVAILABLE(ios(9.0)) {
[self.channel invokeMethod:@"launch" arguments:shortcutItem.type];
[self handleShortcut:shortcutItem.type];
return YES;
}

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if (@available(iOS 9.0, *)) {
UIApplicationShortcutItem *shortcutItem =
Copy link
Contributor

Choose a reason for hiding this comment

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

Why don't we directly call handleShortcut here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have to wait here for the application to become fully active before the method channel are fully initialised. When we directly call the handleShortcut in the didFinishLaunchingWithOptions method the channels on the Dart side are not initialised yet and the invokeMethod:@"launch" isn't processed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah makes sense. Let's add a comment here for future reference.

Why do we return No below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

According to Apple's documentation you should return NO if you already handled the shortcut action which will make sure the application:performActionFor: method and not handle the same action again.

The explanation can be found in the last alinea of the "Discussion" section of this article: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623032-application

Copy link
Contributor

Choose a reason for hiding this comment

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

OK. Let's also add comment here too. Then I think we are good to go!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @cyanglaz, I have added comments as explanation.

launchOptions[UIApplicationLaunchOptionsShortcutItemKey];
if (shortcutItem) {
// Keep hold of the shortcut type and handle it in the
// `applicationDidBecomeActure:` method once the Dart MethodChannel
// is initialized.
self.shortcutType = shortcutItem.type;

// Return NO to indicate we handled the quick action to ensure
// the `application:performActionFor:` method is not called (as
// per Apple's documentation:
// https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622935-application?language=objc).
return NO;
}
}
return YES;
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
if (self.shortcutType) {
[self handleShortcut:self.shortcutType];
self.shortcutType = nil;
}
}

#pragma mark Private functions

- (void)handleShortcut:(NSString *)shortcut {
[self.channel invokeMethod:@"launch" arguments:shortcut];
}

NS_INLINE void _setShortcutItems(NSArray *items) API_AVAILABLE(ios(9.0)) {
NSMutableArray<UIApplicationShortcutItem *> *newShortcuts = [[NSMutableArray alloc] init];

Expand Down
2 changes: 1 addition & 1 deletion packages/quick_actions/quick_actions/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: quick_actions
description: Flutter plugin for creating shortcuts on home screen, also known as
Quick Actions on iOS and App Shortcuts on Android.
homepage: https://github.com/flutter/plugins/tree/master/packages/quick_actions
version: 0.6.0
version: 0.6.0+1

flutter:
plugin:
Expand Down