Messaging between iPhone and Apple Watch with WatchOS2

Tweet Follow @quintonwall

Introduction

During WWDC15, Apple announced WatchOS2. Part of the new OS is a much cleaner pattern for communication between the watch and phone, called Watch Connectivity. There were some great sessions at the conference explaining how to use it, as well as a few super helpful blog posts such as this one from Kristina Thai , however I kept finding that the blogs, or code samples, took you about 90% of the way in order to get messaging to work. They all seemed to leave out the most important part: how to actually create a response and send it directly back to the sender within a code block. I'm not going to cover the different types of messaging (background, etc) as Kristina's post did an excelent job of this, instead will cover the send-response portion.

Let's start on the watch.

Sending a message from the watch

Let's say that as soon as I display a view on my watch, I want to fetch some information from the cloud and display it. With WatchOS2, I need to make sure I can communicate with the phone:

Make your class a delegate of WCSessionDelegate

      class OpportunityTableViewController: WKInterfaceController, WCSessionDelegate {
    

Check to ensure you have a paired phone

      override func willActivate() {
        super.willActivate()

        if (WCSession.isSupported()) {
            session = WCSession.defaultSession()
            session.delegate = self
            session.activateSession()
        }
      }
    

Once the app knows that I have a paired phone, and active app, I can send the message:

    override func willActivate() {
        super.willActivate()

        if (WCSession.isSupported()) {
            session = WCSession.defaultSession()
            session.delegate = self
            session.activateSession()

            let applicationData = ["request-type":"fetchall", "param1": false, "param2": 100000]


            if (WCSession.defaultSession().reachable) {
                session.sendMessage(applicationData, replyHandler: { reply in
                    //handle iphone response here
                    if(reply["success"] != nil) {
                        let a:AnyObject = reply["success"] as! NSDictionary
                        self.loadTableData(a as! NSDictionary)
                    }

                }, errorHandler: {(error ) -> Void in
                    // catch any errors here
                })
            }

        }

    }

My app uses the Salesforce Mobile SDK for retrieving data. In the code snippet above, I set the applicationData payload with a few params to use in a query and pass this via the new sendMessage func included in WCSession. Using a code block, I can handle the reply, or any errors.

The reply comes in the form of an AnyObject. I have found that the easiest approach is cast it the appropriate class type within the block, and then hand the response off to a func. This keeps your block code clean, and any data handling in its own encapsulated function.

Receiving a message from the Watch

Let's look at the other side of this request. The great thing about WatchOS2 vs OS1 is that both sides - the phone and the watch - function exactly the same way:

  1. Activate a paired session
  2. send a message via the sendMessage function.
  3. handle the message using one of the session didReceiveMessage functions
  4. Either send, or receive, the reply via a code block.

We have looked at 1,2, and the receive part of 4. Now, we will look at number 3.

The code snippet below comes from a Helper class I have in my phone app. It has already been set as a WCSessionDelegate and checked for the active session.

    func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
      //make sure we are logged in
       if( SFUserAccountManager.sharedInstance().currentUser == nil) {
            replyHandler(["error": "not logged in"])
       } else {

           let reqType = message["request-type"] as! String

           let sharedInstance = SFRestAPI.sharedInstance()

           if(reqType == "fetchall") {

               let nextQtr = message["param1"] as! Bool
               let maxAmt = message["param2"] as! Int

               let query = getAllOpportunitiesQuery(self, nextQuarter: nextQtr, maxOpportunityAmount: maxAmt)

               sharedInstance.performSOQLQuery(query, failBlock: { error in
                   replyHandler(["error": error])
                   }) { response in  //success
                       replyHandler(["success": response])
               }

           } else {
               replyHandler(["error": "no such request-type: "+reqType])
           }
       }
   }

I mentioned previously that I am using the Salesforce Mobile SDK to fetch data from the cloud. I have left the mobile sdk code in the snippet as it is a great example of performing actions within the reply code blocks (the big thing that I found was missing in other watchOS2 tutorials). Let's decompose the func a bit to help demonstrate how to communicate with the watch.

The first check that is performed is whether the user is actually logged into salesforce, if they are not we can send the response directly back:

    replyHandler(["error": "not logged in"])

ReplyHandler is the secret sauce. All you need to is pass in an NSDictionary and the WatchOS2 takes care of the rest. Throughout the snippet below, this pattern of performing some logic and then using replyHandler to respond.

And, of course, a great practice is the use blocks within your block to send responses back. This is probably the nicest improvement to app dev with watchOS2. WatchOS1 was a little funky with delegates in this space.

Summary

That is about all there is to it. WatchOS2 makes it much easier to register delegates and have them send and respond in a clean, encapsulated way.