Tuesday, April 19, 2016

Watch tutorial 5: Watch Connectivity - Direct Message

This post is part of a set of short tutorials on Watch. If you want to see the previous post. In this tutorial, you're going to see how you can communicate between your Watch and your iOS app.

How does my Watch talk to my phone, and vice versa?

WatchConnectivity framework provides different options for implementing a bi-directional communication between WatchKit and iOS apps.
  • Application Context Mode: allows exchange of data serialized in a dictionary object from one app and another. The transfer is done in the background. The messages are queued and delivered to the receiving app via a delegate method. One specificity of Application Context mode is that only the latest update is sent (ie: older data is overwritten by the new data). This is perfect if the receiving app only need the latest state.
  • User Information transfer mode is similar to application context mode. It is also a background mode, message get queued and unlike application context all messages will be sent once the destination app is available.
  • Interactive messaging mode sends messages (serialized in dictionary) immediately to the receiving app. The receiving app is notified of the message arrival via a delegate method call.
Whether you send a message from your iOS app or from your Watch app, the method to call is the same on both devices. Similarly when you receive a remote call the delegate method to use is the same. You'll get the "déjà vu" feeling when developing with WatchConnectivity especially for bi-directional messages.

Although, there is a symmetry of usage of WatchConnectivity framework, choosing which option to use (queued messages vs direct messages) really depends on your use case and where do you send it from. Time to dig into the nitty-gritty of Direct Messages.

Get starter project

In case you missed Watch tutorial 4: Animation, here are the instructions how to get the starter project. Clone and get the initial project by running:
git clone https://github.com/corinnekrych/DoItCoach.git
cd DoItCoach
git checkout step4
open DoItCoach.xcodeproj

The Use Case

Let's start using WatchConnectivity with this use case in mind. You want to start the task on your Watch and be able to see it as started on your iPhone. From Apple Documentation:

Calling this method from your WatchKit extension while it is active and running wakes up the corresponding iOS app in the background and makes it reachable. Calling this method from your iOS app does not wake up the corresponding WatchKit extension. If you call this method and the counterpart is unreachable (or becomes unreachable before the message is delivered), the errorHandler block is executed with an appropriate error. The errorHandler block may also be called if the message parameter contains non property list data types.

If I call sendMessage(_:replyHandler:) from my Watch, it has the ability to wake-up my iOS app. How cool!

I prefer to use Direct Messages for actions from the watch -> iPhone. The other way around iPhone -> Watch is less useful, as the chances that your Watch app is active when you send DM from your phone is slim.

Direct Message from the Watch to your Phone are useful to say "hi phone, go fetch me these resources", but bear in mind, they are not queued, so if your phone is switch off or out of range, they fail and will not be delivered.

As a rule of thumb, when synchronizing data use Application Context or UserInfo. We could have used ApplicationContext, but we'll design DoITCoach Watch App to be a companion app of the watch. All the states are persisted on the Phone.

Initialise WCSession

WCSession.defaultSession() returns a singleton object. You still need to define which object will handle delegate methods and activate the session.

Where?

For the Watch app, the best place to do it is ExtensionDelegate.swift as this is the place where the life cycle of the app takes place.

How?

In DoItCoach WatchKit App Extension/ExtensionDelegate.swift, add the import:
import WatchConnectivity
var session : WCSession!
func applicationDidFinishLaunching() {
  // Perform any final initialization of your application.
  if (WCSession.isSupported()) {
    session = WCSession.defaultSession()
    session.delegate = self
    session.activateSession()
  }
}
The compiler is now complaining because your class has to implement WCSessionDelegate. In DoItCoach WatchKit App Extension/ExtensionDelegate.swift afte3r the calss definition, add the extension declaration:
extension ExtensionDelegate: WCSessionDelegate {}

Send DM from Watch to Phone

In DoItCoach Watch Extension/InterfaceController.swift add the method to do the send:
func sendToPhone(task: TaskActivity) {
  let applicationData = ["task": task.toDictionary()]
  if session.reachable { // [1]
    session.sendMessage(applicationData, replyHandler: {(dict: [String : AnyObject]) -> Void in
      // handle reply from iPhone app here
      print("iOS APP KNOWS Watch \(dict)")
    }, errorHandler: {(error) -> Void in
      // catch any errors here
      print("OOPs... Watch \(error)")
    })
  } 
}
At the beginning of the file add an import WatchConnectivity.

[1]: As a best practice, you can check the app on iOS device is reachable so you don't waste a call.

Still in DoItCoach Watch Extension/InterfaceController.swift call sendToPhone(_:) in onStartButton() as done in [1] (Note all the onStartButton is unchanged):
@IBAction func onStartButton() {
  guard let currentTask = TasksManager.instance.currentTask else {return} 
  if !currentTask.isStarted() { 
    let duration = NSDate(timeIntervalSinceNow: currentTask.duration)
    timer.setDate(duration)
    // Timer fired
    NSTimer.scheduledTimerWithTimeInterval(currentTask.duration,
                                           target: self,
                                           selector: #selector(NSTimer.fire),
                                           userInfo: nil,
                                           repeats: false) 
    timer.start() 
    // Animate
    group.setBackgroundImageNamed("Time")
    group.startAnimatingWithImagesInRange(NSMakeRange(0, 90), duration: currentTask.duration, repeatCount: 1)
    currentTask.start()
    startButtonImage.setHidden(true) 
    timer.setHidden(false) 
    taskNameLabel.setText(currentTask.name)
    sendToPhone(currentTask) // [1]
  }
}
You also need to send a message to your phone once the task is finished in [1]:
func fire() {
  timer.stop()
  startButtonImage.setHidden(false)
  timer.setHidden(true)
  guard let current = tasksMgr.currentTask else {return}
  print("FIRE: \(current.name)")
  current.stop()
  group.stopAnimating()
  // init for next
  group.setBackgroundImageNamed("Time0")
  display(tasksMgr.currentTask)
  sendToPhone(current) // [1]
}

Receive Message in iOS app


Where?

For the iOS app, the best place to do it is AppDelegate.swift as this is the place where the life cycle of the app takes place. You want to be able to receive direct message even when your iOS app is not started.

How?

var session : WCSession!
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
  if (WCSession.isSupported()) {
    session = WCSession.defaultSession()
    session.delegate = self
    session.activateSession()
  }
  return true
}
Don't forget to import WatchConnectivity.
Déjà vu feeling?
;)

Delegate implementation

// MARK: WCSessionDelegate
extension AppDelegate: WCSessionDelegate {
  func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
    print("RECEIVED ON IOS: \(message)")
    dispatch_async(dispatch_get_main_queue()) { // [1]
      if let taskMessage = message["task"] as? [String : AnyObject] {
        if let taskName = taskMessage["name"] as? String {
          let tasksFiltered = TasksManager.instance.tasks?.filter {$0.name == taskName}
          guard let tasks = tasksFiltered else {return}
          let task = tasks[0]                   // [2]
          if task.isStarted() {
            replyHandler(["taskId": task.name, "status": "already started"])
            return
          }
          if task.endDate != nil {
            replyHandler(["taskId": task.name, "status": "already finished"])
            return
          }
          if let endDate = taskMessage["endDate"] as? Double {
            task.endDate = NSDate(timeIntervalSinceReferenceDate: endDate)
            replyHandler(["taskId": task.name, "status": "finished ok"])
            NSNotificationCenter.defaultCenter().postNotificationName("TimerFired", // [3]
                                                 object: ["task":self])
          } else if let startDate = taskMessage["startDate"] as? Double {
              task.startDate = NSDate(timeIntervalSinceReferenceDate: startDate)
              replyHandler(["taskId": task.name, "status": "started ok"])
          }
          saveTasks()    // [4]
        }
      }
    }
  }
}
[1]: You need to dispatch to main thread as eventually we want to refresh the UITableView in the UI queue.
[2]: You get the task name from the dictionary. You find the matching task in iOS app (task name is used as an identifier).
[3]: You set either startDate or endDate on the task itself. When you end the task, you need to issue an event so that UITableView get refreshed.
[4]: You save all tasks.

Get final project

If you want to check the final project, here are the instructions how to get it.
cd DoItCoach
git checkout step5
open DoItCoach.xcodeproj


Build and Run

Before launching the app, delete any previous version of DoItCoach on your Phone.




What's next?

With this tutorial, you saw how you can send direct messages from your phone to your watch. As you've seen, you can do a lot with direct message: wake up an iOS app but there are still cases where your message won't reach your phone. When it comes to synchronise states between AppleWatch and its iPhone companion app, Application Context or User Info transfer mode are much more suitable. See Watch tutorial 6: Watch Connectivity (Application Context) to learn more.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.