Tuesday, April 19, 2016

Watch tutorial 6: Watch Connectivity - Application Context

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 using Application Context.

Get starter project

In case you missed Watch tutorial 5: Watch Connectivity - Direct Message, 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 step5
open DoItCoach.xcodeproj

Send Application context from Phone

In DoItCoach/DetailedTaskViewController.swift, search for the method timerStarted(_:), and add one line of code in [1]:
func sendTaskToAppleWatch(task: TaskActivity) {
  if WCSession.defaultSession().paired && delegate.session.watchAppInstalled {    // [1]
    try! delegate.session.updateApplicationContext(["task": task.toDictionary()]) // [2]
  }
}
[1]: Before sending to the Watch, as a best practice, check is the Watch is paired and the app in installed on the Watch. No need to do a context update when it's doomed to failure.
[2]: You send the Context update. Here your don't try catch, but you could do it and display the error.

You need to import WatchConnectivity to make Xcode happy.
Still in DoItCoach/DetailedTaskViewController.swift call sendTaskToAppleWatch(_:) in timerStarted(_:) as done in [1] (Note all the rest of the method is unchanged):
@objc public func timerStarted(note: NSNotification) {
  if let userInfo = note.object, 
     let taskFromNotification = userInfo["task"] as? TaskActivity 
     where taskFromNotification.name == self.task.name {
     if let sender = userInfo["sender"] as? String 
         where sender == "ios" {
         task.start()
         sendTaskToAppleWatch(task) // [1]
     }
     saveTasks()
     self.startButton.setTitle("Stop", forState: .Normal)
     self.startButton.setTitle("Stop", forState: .Selected)
     self.circleView.animateCircle(0, color: taskFromNotification.type.color, 
                                   duration: taskFromNotification.duration)
  }
  print("iOS app::TimerStarted::note::\(note)")
}

Receive Message in Watch app

In DoItCoach WatchKit Extension/ExtensionDelegate.swift, at the end of the class definition, add the following extension declaration:
// MARK: WCSessionDelegate
extension ExtensionDelegate: WCSessionDelegate {
  func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
    if let task = applicationContext["task"] as? [String : AnyObject] { // [1]
      if let name = task["name"] as? String,
         let startDate = task["startDate"] as? Double {
         let tasksFound = TasksManager.instance.tasks?.filter{$0.name == name} // [2]
         let task: TaskActivity?
         if let tasksFound = tasksFound where tasksFound.count > 0 {
           task = tasksFound[0] as TaskActivity
           task?.startDate = NSDate(timeIntervalSinceReferenceDate: startDate)  // [3]
           dispatch_async(dispatch_get_main_queue()) {  // [4]
             NSNotificationCenter.defaultCenter().postNotificationName("CurrentTaskStarted", 
                                                                       object: ["task":task!])
           }
         }
       }
     }
  }
}
[1]: You get the dictionary definition of the task that was started on the iPhone.
[2]: You find its matching Task object in the list of tasks in the Watch.
[3]: You assign the startDate defined on the iOS app.
[4]: You make sure you go to UI thread to send a notification for the Watch to refresh its display.

Refreshing Watch display

In DoItCoach WatchKit Extension/InterfaceController.swift in awakeWithContext(_:), add one line of code [1] to register to the event CurrentTaskStarted:
override func awakeWithContext(context: AnyObject?) {
  super.awakeWithContext(context)
  NSNotificationCenter.defaultCenter()  // [1]
                      .addObserver(self, 
                                   selector: #selector(InterfaceController.taskStarted(_:)), 
                                   name: "CurrentTaskStarted", 
                                   object: nil)
  display(TasksManager.instance.currentTask)
}
Still in DoItCoach WatchKit Extension/InterfaceController.swift implement the following methods to respond to the NSNotificationCenter event:
func taskStarted(note: NSNotification) { 
  if let userInfo = note.object,  
     let taskFromNotification = userInfo["task"] as? TaskActivity,
     let current = TasksManager.instance.currentTask
     where taskFromNotification.name == current.name { 
    replayAnimation(taskFromNotification)           // [1]
  }
}
    
func replayAnimation(task: TaskActivity) {
  if let startDate = task.startDate  {
    let timeElapsed = NSDate().timeIntervalSinceDate(startDate) 
    let diff = timeElapsed < 0 ? abs(timeElapsed) : timeElapsed
    let imageRangeRemaining = (diff)*90/task.duration   // [2]
    self.group.setBackgroundImageNamed("Time")
    self.group.startAnimatingWithImagesInRange(NSMakeRange(Int(imageRangeRemaining), 90), 
               duration: task.duration - diff, repeatCount: 1) // [3]
  }
}
[1]: For the current task, replay the animation.
[2]: Calculate how much is images is already started. You will have a short delay since the task was started in the iPhone and you received it on the Watch.
[3]: As you've seen in Tutorial3: Animation, launch the animation.

Build and Run

You can now start a task from your phone. The careful reader that you are, will notice that once a task started from the phone is completed, it is not refreshed on the Watch app. That brings us to the next section, let's talk about your challenges.

Challenges left to do

Your mission, should you choose to accept it is:
  • make the task list refreshed on the Watch when a task started from your phone get completed
  • remove the bootstrap code in TaskManager.swift. All tasks should be persisted to the iPhone (all the persistence code is already written for you in Task.swift). When the iPhone app launch send the list of tasks to Watch. Whenever a task is added on the phone, send the list of tasks to the watch.
  • make the animation carries on where it should be when the Watch app go background and foreground again.

Get final project

If you want to check the final project, here are the instructions how to get it.
cd DoItCoach
git checkout step6
open DoItCoach.xcodeproj
Or if you want to get the final project with all the challenges implemented:
cd DoItCoach
git checkout master
open DoItCoach.xcodeproj

What's next?

With this tutorial, you saw how you can send update application context messages from your Watch to your phone. Since you know how to communicate between your app and your watch, you're all ready to make great apps!

No comments:

Post a Comment

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