Getting Data to Your WatchOS 2 App

Previously I wrote about the various methods I was leveraging in WatchKit to communicate between my iPhone app and my watch app.

With WatchOS 2 all that is out the window. We can now write native apps that run right on the watch.

You can still write a WatchKit based app where the watch app's code executes on the phone and just pushes UI changes to the watch, but I see little future going down that path. Instead I'd recommend writing a WatchOS-based app where all the code actually executes on the watch. This should dramatically speed up the responsiveness of your apps, and the changes to get there aren't too painful.

Going this route means your data has to be kept in sync using bluetooth. You can't rely on shared app group containers, CFNotificationCenter, or shared CoreData stores.

WCSession

Sessions are the root of all your communication to and from your WatchOS app, and the great news is that sessions natively support two-way communication. The session manages all the high-level state information for you: if you're on a device that can have a watch paired, if your app is installed on the watch, if your app is on-screen, and more.

Sessions also have a delegate which handle data received by the device. We'll get into sending/receiving later, but it's important to note that sessions are singletons, so plan your delegate wisely as you'll likely only have one per app (one on the iPhone, one on the watch).

(you can access the session and send data through it from anywhere in your app, but you'll only have one place responsible for receiving information from the other device)

if ([WCSession isSupported]) {
    WCSession *watchSession = [WCSession defaultSession];
    watchSession.delegate = self;
    [watchSession activateSession];
}

Activating the session lets the system know your app is ready to send and receive data. Don't forget to do this on both ends, otherwise your data won't go through!

You'll do this in both your apps, iPhone and WatchOS. I have mine configured in the AppDelegate on iOS and ExtensionDelegate on WatchOS, but it can realistically happen anywhere in your app.

State Syncing

WatchOS is intelligent with sending data to and from the watch so as not to kill battery life. It looks for opportune times to send data over for apps via bluetooth -- for example when it already has to open a connection to send some system data. It has to be smart because after all we're not only sending over UI events anymore when we know the user is using our Watch app, we can be sending data all the time in the background.

Lets say we have a fitness tracking app, and we want to sync the started / paused / running state of the GPS tracking. Our watch app probably doesn't need to know every event that happened since the user last looked at the watch (started -> paused -> resumed -> paused -> resumed), it just needs to answer "what's the state right now?"

WCSession has a method on it called updateApplicationContext, which can be thought of as a way to keep the state of your application up-to-date. Dictionaries you pass into this method are queued up by WatchOS to send over to the other device when it can. If you set a new context before the session had a chance to send it, the old copy is overridden.

NSDictionary *applicationDict = @{@"status" : @"paused"}
[[WCSession defaultSession] updateApplicationContext:applicationDict error:nil];

When you send data this way the other app's delegate will get a call on session:didReceiveApplicationContext: with the latest data.

But, I Need It All

Sometimes you need the stream of data as it happened, you can't let any data be ignored by the system just because a new message came in. WCSession has another method on it for this case, transferUserInfo.

It works very much the same way as updateApplicationContext does except the data is guaranteed to be delivered in a first-in-first-out order.

NSDictionary *applicationDict = @{@"status" : @"paused"}
[[WCSession defaultSession] transferUserInfo:applicationDict error:nil];

You'll get multiple callbacks on session:didReceiveUserInfo: on the other device as all the data comes in.

Instant Gratification

The methods I've covered so far are great for background transfers, helping the users launch your app with up-to-date information, but sometimes you need to send data to the other device instantly. Something more like openParentApplication:reply: from WatchKit.

A great example would be in our hypothetical running app: the user pauses their run using the watch. We don't want to override the global state and have the iPhone look for that, we want to tell the iPhone "hey, pause!" and let it manage any state changing it normally does.

Sessions have a method on them called sendMessage that let you do just this. Similar to openParentApplication:reply: it takes a dictionary of data, and allows for a reply handler so the other device can instantly send data back.

Unlike openParentApplication:reply:, this can be triggered from either device. Both from the iPhone -> Watch and Watch -> iPhone! There are some requirements for messages to go thru though:

  • If sending iPhone -> Watch: the watch must have your app installed, and it must be active on-screen on the watch. This will not wake up your watch app.
  • Sending Watch -> iPhone: the phone must be paired and reachable via bluetooth. This will still wake your app up on the iPhone if it is not already running (although you'll still be killed if you don't normally have background modes to support like audio or GPS).

But how do you know if it's safe to use messaging or a background transfer? WCSession has a property isReachable that you can check to see if these conditions are met. If you aren't reachable, fall back to background messaging.

Lets look at an example of pausing from the WatchOS app:

if ([[WCSession defaultSession] isReachable]) {
    NSDictionary *applicationDict = @{@"action": @"pause"}
    [[WCSession defaultSession] sendMessage:applicationDict replyHandler:^(NSDictionary *replyHandler) {
      //if the iPhone sends back an "OK" response, do something...
    }
    errorHandler:^(NSError *error) {
      //explode
    }];
} else {
  //we aren't in range of the phone, they didn't bring it on their run
}

On my iPhone app my WCSessionDelegate will get called on session:didReceiveMessage:replyHandler: with a copy of that message dictionary and I can do whatever I need to do.

Sage Tips Observations

WatchOS 2 gave us many new ways to communicate between Watch and iPhone. How am I putting it all together and what have I learned? (your mileage may vary)

  • Don't forget to activate your sessions.
  • Don't forget the isReachable rules.
  • I've noticed that sessions seem to be near-instant at sending data through applicationContext and userInfo when the watch app is on-screen, and only throttles when your watch app is in the background. If your watch app launches from the background, the session will kick over the most up-to-date application context almost instantly.
  • Think about how independent your WatchOS app can be. Quite a few of mine can't operate without the phone, so I let the phone manage the data and just use applicationContext / userInfo to keep the data in sync to the watch.
  • If your watch app can manage state while disconnected, you're writing a sync system; good luck! In those cases I'm relying on userInfo background sends to replay changes from each device when I finally get them. The system will keep the change log queued until they are connected again, so I can rely on the data getting through (system crashes aside).
  • I reserve sendMessage for actions from the watch -> iPhone. Sending data through applicationContext/userInfo when the app is on-screen is fast, so I think of sendMessage as only "here is an action I need the other device to respond to and update data as they see fit" vs userInfo of "data changed on my end, here's how it changed you should sync up."