What I need
I want to convert an async system API to a synchronized one and call it inside SceneDelegate’s continueUserActivity
.
Note: I fully understand that iOS will kill the app if I don’t return within a few seconds. But in my case I have to wait till the system API completion in order to continue this function.
What’s working
I have the following code:
// Imagine this is a system async API that (1) has to be called on main thread, and (2) it also completes on main thread.
- (void)systemAsyncAPI:(void(^)(void))completion {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
completion();
});
}
// Convert the system API from async to sync
- (void)synchronizedSystemAPI {
__block BOOL completed = NO;
[self systemAsyncAPI:^{
completed = YES;
}];
while (!completed) {
NSLog(@"Tick");
[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}
NSLog(@"completed");
}
Now when I use this code inside SceneDelegate’s willConnectToSession
:
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
[self synchronizedSystemAPI];
}
Everything works as expected, with the following log:
Tick
Tick
Tick
completed
What’s not working
Now, I need to call this synchronizedSystemAPI
inside SceneDelegate
‘s continueUserActivity
instead of willConnectToSession
.
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity {
[self synchronizedSystemAPI];
// Continue work after system API is complete
}
This code now has a deadlock, as it ticks forever:
Tick
Tick
Tick
Tick
Tick
Tick
Tick
Tick
Tick
...
My understanding
Inspired by this answer, I think the reason is, willConnectToSession
is called in old technique (predates GCD) that is not a dispatch item. continueUserActivity
is part of a dispatch item on main queue, hence the deadlock, as explained in that linked answer.
How do I test the code?
An easy way to invoke continueUserActivity
is via spotlight search:
- add
NSUserActivityTypes
array in info.plist. Here I put a single item “foo”.
- setup the activity:
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:@"foo"];
activity.title = @"foo title";
activity.eligibleForSearch = YES;
[activity becomeCurrent];
}
- Run the app.
- Go to home screen and search “foo title” in spotlight search.
- You should be able to this app. Tap on it to resume
- Notice
continueUserActivity
will be called.
And here is my full reproducible code. Don’t forget to add the activity item in Info.plist as described above.
@implementation SceneDelegate
// A system async API that has to be called on main thread, and it also completes on main thread.
- (void)systemAsyncAPI:(void(^)(void))completion {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
completion();
});
}
// Convert the above system API from async to sync
- (void)synchronizedSystemAPI {
__block BOOL completed = NO;
// Imagine this is a system API that must be called on main thread, and completes on main thread.
[self systemAsyncAPI:^{
completed = YES;
}];
while (!completed) {
NSLog(@"Tick");
[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}
NSLog(@"completed");
}
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:@"foo"];
activity.title = @"foo title";
activity.eligibleForSearch = YES;
[activity becomeCurrent];
}
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity {
[self synchronizedSystemAPI];
}
@end