Communicating between iOS Apps and WatchKit Extensions

Mohammed Safwat
Mohammed Safwat
Share

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.

Frequently Asked Questions (FAQs) about Passing Messages between iOS Apps and WatchKit Extensions

How does MMWormhole work in passing messages between iOS apps and WatchKit extensions?

MMWormhole is a library that creates a bridge between iOS apps and WatchKit extensions, allowing them to communicate and pass messages. It uses CFNotificationCenter for message passing and NSFileCoordinator for data synchronization. When a message is passed, it is written to a shared file, and a notification is posted to the CFNotificationCenter. The receiver listens for these notifications and reads the message from the shared file.

What are the prerequisites for using MMWormhole?

To use MMWormhole, you need to have a basic understanding of iOS development and WatchKit extensions. You also need to have the latest version of Xcode installed on your system. Additionally, you should be familiar with Swift or Objective-C, as MMWormhole supports both languages.

How do I install MMWormhole in my project?

MMWormhole can be installed using CocoaPods or Carthage. For CocoaPods, add pod 'MMWormhole', '~> 2.0' to your Podfile and run pod install. For Carthage, add github "mutualmobile/MMWormhole" to your Cartfile and run carthage update.

How do I send a message using MMWormhole?

To send a message, you first need to create an instance of MMWormhole. Then, you can use the passMessageObject(_:identifier:) method to send a message. The identifier parameter is a string that uniquely identifies the message.

How do I receive a message using MMWormhole?

To receive a message, you need to listen for it using the listenForMessageWithIdentifier(_:listener:) method. The listener parameter is a closure that is called when a message with the specified identifier is received.

Can I use MMWormhole to pass messages between two iOS apps?

Yes, MMWormhole can be used to pass messages between two iOS apps. However, both apps need to have the same App Group configured in order to share data.

What types of data can I pass using MMWormhole?

MMWormhole supports passing any type of data that can be serialized into NSData. This includes basic types like strings and numbers, as well as more complex types like arrays and dictionaries.

How do I handle errors in MMWormhole?

MMWormhole does not provide built-in error handling. However, you can add your own error handling code in the listener closure. For example, you can check if the received message is of the expected type and handle the error if it is not.

Can I use MMWormhole in a multi-threaded environment?

Yes, MMWormhole is thread-safe and can be used in a multi-threaded environment. It uses NSFileCoordinator to ensure that file operations are atomic and thread-safe.

Is MMWormhole compatible with watchOS 2 and later?

Yes, MMWormhole is compatible with watchOS 2 and later. However, for watchOS 2 and later, you need to use the MMWormholeSession class instead of the MMWormhole class.