Getting data to your WatchKit app
Update Aug 27, 2015: I've written an updated post for WatchOS 2+. This article will still work with WatchKit based apps running on WatchOS 2, but if you're planning on writing a native WatchOS app check the new article out.
With WatchKit beta 2 Apple provided an easy new way for developers to ask their iPhone app (what Apple is calling the "parent" app) for data: openParentApplication:reply:
on WKInterfaceController
. That works great for many situations, but Apple has also recommended the use of the C-level Dawrin notification center for communication.
Add these on top of the existing use of NSUserDefaults initWithSuite:
that was introduced for extension communication and we're got a lot of ways to communicate.
Method 1: Marco, Polo: OpenParentApp
openParentApplication:reply:
is a two-way communication system, but it can only be triggered from the WatchKit extension. That's a pretty big gotcha, but most of the time it works out OK.
In your WatchKit extension you can call openParentApplication:reply:
on WKInterfaceController
and pass along a dictionary to your main app. You'll probably want to include details on why your parent app is being called, and any other data the iPhone app would need to respond. You'll get back a dictionary of data as a reply:
[WKInterfaceController openParentApplication:@{@"action" : @"getUserCount"} reply:^(NSDictionary *replyInfo, NSError *error) {
if (error) {
NSLog(@"Error from parent: %@", error);
} else {
//do something with the reply info....
}
}];
When you call the above your iPhone app is launched and application:handleWatchKitExtensionRequest:reply:
on your appdelegate is passed that dictionary you set up. 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 {
//look at the userInfo dictionary to figure out why we're being called
//do some stuff
//send back a dictionary of data to the watchkit extension
reply(@{@"numberOfUsers": @(5)})
}
Pros and cons
There are some very nice advantages to this method. You might have noticed that I said your iPhone app will be launched? The biggest advantage here is that your app will be called and given the opportunity to respond, even if it wasn't currently running.
Another great use for this method of communication is asking your app to do something. For example you might have a music app that's running in the background and you want to pause it. openParentApplication:reply:
is perfect for that -- you don't care about the reply, you'll just use the dictionary to pass a pause command.
One important gotcha is that while this will launch your app if needed, it won't bring your app to the foreground; the app is launched in the background state (this means it's impossible to force your UI to appear if it isn't already on-screen). If your app wasn't already running it will be terminated after you reply to the request, unless you trigger a background mode to start (location updates, audio, etc).
This can be a little tricky because all the normal app lifecycle stuff will still happen -- application:willFinishLaunchingWithOptions:
is still called, your storyboard is loaded, etc (more on that in a second). So just be aware that while your UI code will be loaded it can't be seen unless the user already had your app open.
It can be a little annoying to have all your viewcontroller code start up when the view is loaded but not shown (maybe you start a network request when your main view is displayed)...
Protip to stop UI from loading in background launches
At the time of writing all normal app lifecycle stuff is triggered, storyboard loading / etc, causing all my viewwillappear:
etc to be called. I worked around this by only loading my storyboard after first checking to make sure we aren't being launched in a background state in application:didFinishLaunchingWithOptions:
:
if (application.applicationState != UIApplicationStateBackground) {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"myStoryboardName" bundle:nil];
self.window.rootViewController = [storyboard instantiateViewControllerWithIdentifier:@"myRootController"];
}
This does require you to remove the main storyboard entry from Info.plist and maunally load it yourself old-school style, but it should look familiar to most developers.
Method 2: A Shared Data Cache with NSUserDefaults and App Groups
Sometimes waking up your parent app for data each time you need it is excessive.
Since the release of extensions on iOS8 developers have had a concept called App Groups. Diving into how to set that up would be a whole other post, and a trip to the developer portal, but it's basically a way to share data between multiple executables on the phone in a separate sandbox. You permission your apps and extensions to all be a part of the same group and they can share data through a common sandbox.
Most of the time you're dealing with data, like [NSUserDefaults standardDefaults]
, that's in the app's sandbox. NSUserDefaults
has an alternate constructor, initWithSuite:
that lets you save and read data using the NSUserDefaults API between multiple executables.
(you can also throw CoreData or plain old files into these App Groups, but we'll keep things simple for now)
Beyond changing the init method nothing else really changes from the NSUserDefaults
you're used to. In your iPhone app:
NSUserDefaults *defaults = [NSUserDefaults initWithSuite:@"myGroupName"];
[defaults setInteger:4 forKey@"myKey"];
[defaults synchronize];
And then in your WatchKit extension you can read that data at any time:
NSUserDefaults *defaults = [NSUserDefaults initWithSuite:@"myGroupName"];
NSInteger myInt = [defaults integerForKey@"myKey"];
Pros and Cons
You can use this for any data type you can serialize into NSUserDefaults
. It's a great way for passing less dynamic data back and forth, and you don't have to wake your parent app up to view the data.
This method is also great for sharing data to a Today Widget, which doesn't have the option to open up the parent app and ask for data.
One downside I ran into this method of passing data around, though, is that there is no way to be notified when a value changes. Key-value-observing isn't working. So what happens if you have data that can change as the user is using your WatchKit app? Polling based on an NSTimer
is less than ideal. This is where Apple recommended Dawrin notification center.
Method 3: Sharing Data with Update Callbacks Using Darwin Notification Center
I say Darwin notification center, but really I mean the great CocoaPod MMWormhole that's a lightweight wrapper (you didn't really want to worry about bridging down to a C API, did you?).
Using MMWormhole is very much like using NSUserDefaults
, it even uses the same App Group config, but you get the opportunity to subscribe for updates. The notification aspect of CFNotificationCenter is in-memory, so this is all pretty lightweight.
Lets say we have a running app that shows current speed and we want the UI on the watch to always show the right data. In the iPhone app we'd set up a wormhole and post the current speed whenever it changes:
self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:@"myAppGroup" optionalDirectory:@"wormhole"];
//whenever the speed changes...
[self.wormhole passMessageObject:currentSpeed identifier:@"currentSpeed"];
Then on the watch side we need to init the UI with the current speed. Unlike UIKit for iOS the views are loaded and valid when init
is called in WKInterfaceController
, so that's were we'd set the initial value:
- (instancetype)init {
if (self = [super init]) {
self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:@"myAppGroup" optionalDirectory:@"wormhole"];
NSNumber *currentSpeed = [self.wormhole messageWithIdentifier:@"currentSpeed"];
[self.topSpeedLabel setText:[NSString stringWithFormat:@"%@ MPH", currentSpeed];
}
return self;
}
This is pretty similar to how you'd do things with NSUserDefaults
. The data can change on us behind the scenes after init
, so how do we keep it in sync?
KVO Through the Wormhole
We'll use willActivate
and didDeactivate
on WKInterfaceController
to know when we should update the data. These are conceptually a combination of viewWillAppear
and viewDidDisappear
on iOS, plus notification for when the screen turns on and off (remember Apple Watch will shut off the screen to save power if the user isn't interacting with it).
With these we can subscribe to changes to the current speed, but only while our view is on-screen and the watch screen is on:
- (void)willActivate {
// This method is 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 is 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, just a blank screen) we don't waste time communicating with the phone and updating a UI the user can't see. With WatchKit we'll want to be smart with our UI updates so we don't waste power, so make sure you take advantage of willActivate
and didDeactivate
appropriately.
You can also use MMWormhole with Today Widgets, same as NSUserDefaults
, so you can have a centralized data store for all your widgets with change notification support.
I think many developers are going to be OK just using openParentApplication:reply:
to grab data from their iPhone app as neeed, but the other two methods are pretty powerful and may actually be required in situations where the data can change frequently.
Personally I've refactored Slopes to mostly use MMWormhole, but I use openParentApplication:reply:
to trigger actions like start/pause/resume.
Update 2/12/2015: I expanded on this post as a full talk for Philly Cocoaheads. You can check out the talk on Vimeo.