8

I'm developing an Apple Watch application that uses the openParentApplication:reply: method to communicate with its parent app.

The parent app communicates with a web service and sends back the data it gets to the watch extension by means of calling the reply method with a NSDictionary containing the data.

The app works perfectly when the parent app is open in the foreground or background. But if I open the parent app and then terminate it using the task switcher, the first time the watch extension makes a call to openParentApplication:replyInfo:, it gets the following error and the parameter replyInfo comes in as nil.

UIApplicationDelegate in the iPhone App never called reply()

But every single openParentApplication:replyInfo: call the extension makes after that gets a proper response.

I checked and found out that the first time the watch extension makes the call, the handleWatchKitExtensionRequest:reply: is never get called on the parent app.

What could be the possible reason for this?

I'm performing all operations in the handleWatchKitExtensionRequest:reply: in a background task, as suggested in the docs. Here's some of my code: Code from my extension:

NSDictionary *params = @{@"requestCode": @(RequestGetLoggedIn)};

[WKInterfaceController openParentApplication:params reply:^(NSDictionary *replyInfo, NSError *error) {
    // Do something with the result
}];

Code from the parent app:

- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
    self.backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
    }];

    NSNumber* requestCode = userInfo[@"requestCode"];

    // Perform some request and then call reply()

    // End the background task
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
    });
}

Edit 1: The problem occurs both on the Simulator and on a real Apple Watch.

Cihan Tek
  • 5,349
  • 3
  • 22
  • 29
  • Maybe [my answer here](http://stackoverflow.com/questions/29750310/watchkit-handlewatchkitextensionrequest-multiple-instances/29765448#29765448) can help you – zisoft Jul 03 '15 at 18:01
  • I don't think that's the case here because everything works properly after the first call. If there was a problem in `application:didFinishLaunchingWithOptions:`, then the app could never have opened in the background and hence the call would never get any response. – Cihan Tek Jul 03 '15 at 19:05
  • In response to edit #2: Why would starting the background task at the end of the method help? You need to start the background task _immediately_ and perform your work in it. I don't think this is a bug in iOS. It sounds like your app is being killed (due to the lack of a background task) before it has a chance to reply. – bgilham Jul 07 '15 at 03:29
  • Edit 2 was a response to the answer below, and yes as expected, it didn't change anything so I've just removed it. Regarding your comment, in my code above, I'm already starting the task immediately, before performing anything else. Also if the problem was getting killed in the background, then my call would always fail, not only the first time. The log in my answer below strongly suggests that this is due to erratic behavior of the OS. Parent app cannot send back a response if it is started only after the extension gives the error. – Cihan Tek Jul 07 '15 at 05:50

2 Answers2

5

It looks like there's a bug in iOS 8.4.

I've added NSLog's to the beginning of application:didFinishLaunchingWithOptions: and handleWatchKitExtensionRequest:reply:, performed the actions that lead to the problem and then checked the device log and got this:

--- Notice>: (Warn ) WatchKit: <SPCompanionAppServer.m __91-[SPCompanionAppServer launchCompanionAppForGizmoAppWithIdentifier:withUserInfoData:reply:]_block_invoke_2:1450> Got BSActionErrorCodeResponseNotPossible for com.xyz.xyz.watchkitapp. This will translate to WatchKitApplicationDelegateWatchKitRequestReplyNotCalledError

... Irrelevant stuff

--- WatchKit Extension[1686] <Warning>: __59-[InformationController getNotificationListIncremental:]_block_invoke (null)
**--- <Warning>: MY LOG: Application did launch with parameters (null)**

This log shows that application:didFinishLaunchingWithOptions: gets called AFTER the OS gives an error about not getting a response from the parent app. How's the app gonna give a response if it's not getting launched first?

I've temporarily solved the problem by calling the openParentApplication:reply: method again when this problem occurs.

The way I've implemented the retry once behaviour is by creating a method that wraps the call and using that one instead the original method. I added this as a class method to a utility class, but it can be a global function as well.

+ (void)openParentApplication:(NSDictionary*)params reply:(void(^)(NSDictionary *replyInfo, NSError *error))reply
{
    [WKInterfaceController openParentApplication:params reply:^(NSDictionary *replyInfo, NSError *error) {
        if (error.domain == WatchKitErrorDomain && error.code == WatchKitApplicationDelegateWatchKitRequestReplyNotCalledError)
        {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [WKInterfaceController openParentApplication:params reply:^(NSDictionary *replyInfo, NSError *error) {
                    reply(replyInfo, error);
                }];
            });
        }
        else
            reply(replyInfo, error);
    }];
}
Cihan Tek
  • 5,349
  • 3
  • 22
  • 29
  • This appears to have started with iOS8.4, and while this retry suggestion appears to work, I had better luck moving away from openParentApplication altogether in favor of doing the work in the watchkit extension. I found the WWDC "Watchkit Tips and Tricks" video very helpful in this regard. Since openParentApplication is being deprecated in WatchOS2, my approach should pay dividends. – CSmith Jul 10 '15 at 18:03
  • I think I've experienced the same behavior in 8.3 as well. WatchKit connectivity isn't available prior to iOS 9.0, so unless the parent app doesn't support any prior versions of iOS, we won't be able to use WatchOS2, and hence have to rely on openParentApplication. – Cihan Tek Jul 10 '15 at 18:05
  • 2
    instead of if (replyInfo == nil), you can use (error.domain == WatchKitErrorDomain AND error.code == WatchKitApplicationDelegateWatchKitRequestReplyNotCalledError) – CSmith Jul 10 '15 at 18:08
  • @Cihan, you can do the work in the watch extension, but it requires more extensive re-engineering (e.g. using file/keychain access groups, framework libraries, etc). Anyhow, its worth watching that WWDC video, you're going to be forced by Apple to move away from openParentApplication eventually anyway. Thanks for the post! – CSmith Jul 10 '15 at 18:12
  • @CSmith, I've watched the session you're referring to, but I don't think they can completely replace what openParentApplication does in WatchOS 1. I agree that we'll leave openParentApplication eventually, but unfortunately not before dropping support for iOS 8.x. Any alternative method will have to be replaced by Watchkit Connectivity anyway. Apple really did a poor job with WatchOS 1. It's a half-baked product. WatchOS 2 is what WatchOS 1 should have been. It'll provide an infinitely better user experience by doing everything on the watch. – Cihan Tek Jul 10 '15 at 19:27
  • You may want to have a more robust String comparison :`if ([error.domain isEqualToString: WatchKitErrorDomain] && error.code == WatchKitApplicationDelegateWatchKitRequestReplyNotCalledError)` – blackbox Aug 14 '15 at 12:07
  • Since the error domains are defined as static constant `NSString`s and therefore the same domain value will always correspond to the same memory location, that's not necessary. – Cihan Tek Aug 14 '15 at 14:45
0

I am getting the same issue. See the following bare bones project I made for a bug report to Apple: https://www.dropbox.com/s/ayltpprjck37ins/HandleWatchkitExtensionError%202.zip?dl=0.

@Cihan Tek - how are you calling openParentApplication:reply: again? Are you calling it in the reply block? I am still receiving the error when I do that.

user3091519
  • 107
  • 1
  • 12
  • 1
    Thanks @CihanTek. I noticed that if you try calling openParentApplication:reply: before applicationDidFinishLaunching is finished, you will still receive an error. I had to add a slight delay to my second call to make sure everything is initialized. – user3091519 Jul 10 '15 at 16:00