Mobile
Article

Communicating between iOS Apps and WatchKit Extensions

By Mohammed Safwat

Communicating between iOS Apps and WatchKit Extensions

Apple Watch applications that need data updates like stocks, weather, sports and transport information introduce communication methods between the parent iOS application and the WatchKit extension.

In this article I’ll show how to create this communication and introduce real-time messaging between them.

Let’s look at an overview on the communication between the iOS application and its WatchKit extension, and the different states the iOS application and WatchKit extension can be in when you need to share data.

openParentApplication:reply:

With WatchKit beta 2, Apple introduced openParentApplication:reply: and the corresponding application:handleWatchKitExtensionRequest:reply UIApplicationDelegate method. The openParentApplication:reply: method can be called inside any WatchKit Interface Controller, and application:handleWatchKitExtensionRequest:reply will be called inside the iOS application’s AppDelegate file.

openParentApplication:reply: can pass a dictionary to the main app that can include any data the parent iOS app needs to respond to. This allows the WatchKit extension to wake the containing iOS application from an Apple Watch if it’s suspended or not running:

[WKInterfaceController openParentApplication:@{@"action" : @"actionName"} reply:^(NSDictionary *replyInfo, NSError *error) {
    if (error) {
        NSLog(@"An error happened while opening parent application: %@", error);
    } else {
         //do something..
    }
}];

After the iOS application launches, application:handleWatchKitExtensionRequest:reply: passes the dictionary sent with openParentApplication:reply:. Inside handleWatchKitExtensionRequest:reply: you will need to set up any action(s) that the iOS application should run. Such as sending a RESTful HTTP request, or anything needed to reply to the WatchKit extension. You’re expected to call the reply block when you’re done to let the system know you’ve completed any task you needed to execute to complete the response:

- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply {
        //do some stuff
        //send back a dictionary of data to the watchkit extension
        reply(@{@"dataToReturn": @"dataToReturnValue"})
}

The reply block allows the iOS application to return data to the WatchKit extension.

This method can help if, for example you are developing a music application that will run in the background and you want to control it from the Apple Watch. You will use openParentApplication:reply: and just use the dictionary to pass a pause, stop or resume command.

One important note is that if the parent iOS application wasn’t already running it will terminate after you reply to the request. Unless, for example, you trigger a background mode to start location updates or audio. The openParentApplication:reply: method will launch the app if needed, but it won’t bring it to the foreground. The app is launched in a background state and this means that it’s impossible to force the UI to appear if it isn’t already on-screen.

NSUserDefaults

Another method to create communication between the iOS application and its WatchKit extension is to use NSUserDefaults. NSUserDefaults is an API that sits on top of a plist file. The App Groups concept was introduced with the release of extensions in iOS 8. The parent iOS application and extension can share data through a common sandbox by making both a part of the same group. NSUserDefaults has a constructor, initWithSuite: that lets you save and read data using the NSUserDefaults API between executables.

Beyond changing the init method, nothing else changes from the NSUserDefaults you’re used to. In the iPhone app you can set the data you need to store in NSUserDefaults:

NSUserDefaults *defaults = [NSUserDefaults initWithSuite:@"appGroupName"];
[defaults setInteger:4 forKey@"myKey"];
[defaults synchronize];

Inside the WatchKit extension you can read the data at any time:

NSUserDefaults *defaults = [NSUserDefaults initWithSuite:@"appGroupName"];
NSInteger myInt = [defaults integerForKey@"myKey"];

This is a great method for passing data between the WatchKit extension and its parent iOS application, especially if you don’t need to wake the parent application to view the data. Note that you can serialize any data type into NSUserDefaults. This is ideal for sharing data to a Today Widget, which doesn’t have the option to open up the parent app and ask for data.

Darwin Notification Center

In some situations a notification is needed when a value changes, and the previous methods don’t achieve this. There can be data on the parent iOS application that changes while the user is using the WatchKit application. For situations that need real-time data updates, Apple recommended the use of Darwin Notification Center.

There’s a popular library that I used for situations like this, it’s called MMWormhole. MMWormhole supports CFNotificationCenter Darwin Notifications to support real-time change notifications. MMWormhole uses NSKeyedArchiver as a serialization medium, so any object that is NSCoding compliant can work as a message. Messages can be sent and persisted as archive files and read later when the app or extension wakes up. MMWormhole works whether the containing app is running or not, but notifications will only trigger in the containing app if the app is awake in the background. This makes MMWormhole ideal for cases where the containing app is already running via some form of background modes.

MMWormhole depends on having interested parties that can listen and be notified of messages sent from other parties. The effect is nearly instant updates on either side when a message is sent through the wormhole. Using MMWormhole is much like using NSUserDefaults and it uses the same App Group configuration.

Let’s say that we have an iOS application updating the remaining time for a bus to arrive and you need to have the same data updated instantly on the Apple Watch application too. In the iOS application, we can set up a wormhole and update the remaining time whenever it changes:

self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:@"myAppGroup" optionalDirectory:@"wormhole"];
//whenever the time changes...
[self.wormhole passMessageObject:currentSpeed identifier:@"currentRemainingTime"];

With the WatchKit extension we can update the current remaining time value inside the init method. Unlike UIKit for iOS the views load and validate when init is called inside WKInterfaceController:

- (instancetype)init {
    if (self = [super init]) {
      self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:@"myAppGroup" optionalDirectory:@"wormhole"];
      NSNumber *currentRemainingTime = [self.wormhole messageWithIdentifier:@"currentRemainingTime"];
      [self.remainingTimeLabel setText:[NSString stringWithFormat:@"%@ Seconds", currentRemainingTime];
    }
    return self;
}

This is close to how we accomplish things with NSUserDefaults. There are cases where the Apple Watch will shut off the screen when the user is not interacting with it (to save power). This will make our Apple Watch application deactivated. When the user taps on our application again and starts using it the Apple Watch’s application state becomes activated again. In this case, we should start listening again for any incoming messages because in the above example, we have done our work inside the init method, but data can change behind the scenes after init. For this situation, use the willActivate and didDeactivate methods on WKInterfaceController to know when we should update the data. With these methods we can get any changes but only while our view is on-screen and the watch screen is on:

- (void)willActivate {
    // This method gets called when watch view controller is about to be visible to user
    [super willActivate];
    [self.wormhole listenForMessageWithIdentifier:@"currentSpeed" listener:^(id messageObject) {
    [self.topSpeedLabel setText:[NSString stringWithFormat:@"%@ MPH", (NSNumber *)messageObject];
    }];
}


- (void)didDeactivate {
    // This method gets called when watch view controller is no longer visible
    [super didDeactivate];
    [self.wormhole stopListeningForMessageWithIdentifier:@"currentSpeed"];
}

This means that while the screen is off (our app may still be alive, but a blank screen) we don’t want to waste power communicating with the parent iOS application and updating a UI the user can’t see.

In the next part of this short series, I’ll be coding a small to-do list application from scratch that implements the different methods of communication explained above. In the meantime, let me know if you have any questions in the comments below.

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Mobile, once a week, for free.