Pushing Data to Apple Watch Complications via PushKit

Last year at WWDC, Apple introduced watchOS 2 and with it the ability to build native apps for the Apple Watch. In addition, we were now able to create custom “complications” (complications are a watch-world term for non-time features on your watch face such as your next calendar entry or the current weather) featuring data from our apps.

If you do any work with apps for watchOS you quickly discover the significant restrictions imposed in order to give the Apple Watch all-day battery life.

I remember starting on the iPhone in 2008 and thinking that thing was unforgiving when it came to having to optimize for performance and power. Turns out it was good training for watchOS - the restrictions for the wrist are even more severe. We’re talking about apps that are only allowed to run for seconds at a time. It can be tough but it’s also a fun challenge.

I recently wanted to create a custom watchOS complication for a side-project I’m working on. I discovered that while there is a video from last year’s WWDC that covers the basics, the process of using PushKit to send custom notifications to update the complications was less well documented. It’s technically all covered but it took me awhile to get it to work and I couldn’t find much in the way of resources. Hopefully this helps somebody else trying to do the same thing.

An Overview of the Process

I’ll cover the steps required in order to send data from your server via a push to your complication on the watch. You can also create complications that update on a fixed schedule (i.e. try to update data every 4 hours) but I wanted to have the complication update as certain events took place on my server.

Data moves through the cloud from your server to the Apple Push Notification System (APNS). From there, the APNS sends the push notification to the iPhone and then your app delivers the data to the Apple Watch via a high priority channel. The Apple Watch does not receive the push notification - the iPhone does.

Courtesy Apple, Inc.

The basic steps are as follows:

Getting Setup

As with standard push notifications for an iPhone app, you need to start by generating certificates in the Developer Portal. What I discovered as part of this process was that there is a specific certificate type for this feature: “WatchKit Services Certificate”.

They claim you can use a single certificate to send both these and regular app pushes as long as you’re using the HTTP/2 interface. I didn’t test this and instead setup my app with a distinct certificate for sending the complication pushes. YMMV.

The server-side of this particular app is based on Ruby on Rails and I tried a new push gem because I wanted something that supported the HTTP/2 interface: Apnotic. So far it’s worked like a charm and not having to have a separate set of logic to handle the feedback service sure is nice.

Like many APNS related gems, the README has instructions to create the .pem files you’ll need based on your certificates.

Handling the Token

Once you’ve got the certificate generated and on your server, it’s time to generate the token you’ll use to communicate with the complication.

This requires using PushKit, an iOS 8-introduced framework to handle special kinds of push notifications. Using PushKit is simple - create an instance of the registry and tell it what kinds of pushes you want to do. The delegate message it sends back will provide the token for you to send to your server.

This code lives in your iPhone app, possibly related to other WatchConnectivity code you already have to talk to your watchOS app.

//Create an instance of PKPushRegistry and request pushes of type PKPushTypeComplication
pushRegistry = PKPushRegistry(queue: dispatch_get_main_queue())
pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [PKPushTypeComplication]
//Implement the delegate method to get the generated credentials. In this case, 'networkManager' is an instance of a class that exposes the ability to talk to the network. It handles sending the token to the server.
extension WatchSupport: PKPushRegistryDelegate {
    func pushRegistry(registry: PKPushRegistry!, didUpdatePushCredentials credentials: PKPushCredentials!, forType type: String!) {
        let tokenChars = UnsafePointer<CChar>(credentials.token.bytes)
        var tokenString = ""

        for i in 0..<credentials.token.length {
            tokenString += String(format: "%02.2hhx", arguments: [tokenChars[i]])
        }

        self.networkManager?.submitNotificationDevice(tokenString, deviceType: "apple-watch-complication") //the deviceType helps the server distinguish different push types
    }
}

So now we’ve successfully registered for complication push notifications and our server has the device token it will need to send those pushes. It’s time to send some data!

Pushing Your Data

Say you own an airline (work with me here) and you want to update your customers when their flights are delayed. Once your service has figured out there’s going to be a delay, it’s time to send some complication pushes.

On platforms like Rails, using gems like the previously mentioned Apnotic make this process simple. Figure out what you want to push and you can be sending in just a few lines of code:

content = {data: "my super important data"}

connection = Apnotic::Connection.new(cert_path: "#{Rails.root}/config/apns_complication_cert.pem", cert_pass: "complex")

self.mobile_devices.where(device_type: "apple-watch-complication").each do |device|
  notification = Apnotic::Notification.new(device.device_identifier)
  notification.topic = "com.normal.bundle.id.complication" #huh, what is this? see below
  notification.content_available = 1
  notification.custom_payload = content #the goods

  response = connection.push(notification)

  #do something responsible with the response here, un-enroll bad devices, handle errors, etc...
  #response codes are based on HTTP and documented in APNS docs
end

connection.close

In the above, content is a hash containing the data needed to update the complication. Unlike some other forms of push notifications, you should always be sending the payload data in the push itself. The complication push type has a larger payload size (4kb) and when it arrives on the device there’s no time to do another network request - this is your chance to send the data you will need to update the user’s display.

One thing that fouled me up - the newer APNS interface has some fields to set on your notifications that I hadn’t run into before and that I couldn’t find documented anywhere. Specifically the idea of a 'topic’. Typically it’s the bundle ID of your app but if you’re sending a complication push you need to set it explicitly.

To find the correct value, go look at the certificate you created. In my case it was in the format of: 'com.normal.bundle.id.complication'. Without this set, you will get errors back from the APNS.

Note that these pushes are budgeted - each complication gets only a set number of these per day and if you exceed that budget, you may receive an error. How many is too many? Apple doesn’t say - it likely varies based on current power and network conditions.

Receiving the Push

Your push will now be received on the iPhone via the same PKPushRegistryDelegate:

extension WatchSupport: PKPushRegistryDelegate {
    func pushRegistry(registry: PKPushRegistry!, didReceiveIncomingPushWithPayload payload: PKPushPayload!, forType type: String!) {
      self.session?.transferCurrentComplicationUserInfo(["complication" : payload.dictionaryPayload])
    }
}

In the above case, session is an instance of WCSession from the WatchConnectivity framework. The function used above transfers data to complications in near real-time.

This is another spot where I struggled for a bit, this time because I didn’t pay close enough attention to the WWDC videos explaining this stuff. I had set a breakpoint in the above delegate method but the debugger never fired. I wasn’t getting my pushes and I wasn’t sure why.

Turns out, you will only receive these pushes if you actually have the complication on your active, visible watch face. If not, even if it is on a watch face that’s not active, you’ll simply get nothing back from the APNS system when these pushes are sent. Once I figured that out, I was in business.

Updating the Complication

Now we finally are in a spot where the watchOS app needs to finish the job. The above snippet will send the complication data to a listening WCSession on the other end, in this case in my CLKComplicationDataSource. You need to decode the data and create the complication to display the updated data on the watch face.

As previously noted, you should be sending all the data you need in the push itself. The system gives you only a very brief time to process this data - not enough to fetch more data from the network reliably (plus that would hit your time and power budget hard leading to fewer updates overall).

func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
    if let complicationUserInfo = userInfo["complication"] as? [NSObject : AnyObject], complicationData = ComplicationData.createFromDictionary(complicationUserInfo) {
      //refresh the complication with the decoded data normally
    }
}

At this point, the complication should be showing the updated data.

It’s Complicated!

Well, hopefully not really. This was a fun project to work on, even if it was a bit frustrating at times when I ran up against a question where I couldn’t easily find the answer.

As of this writing we are about a month way from WWDC 2016 where I expect we’ll see watchOS 3 for the first time.

What new fun will it bring us? I’m excited to find out.

Tweet at Hunter

Share this post!