Chapter 2. Apple Watch

Version 2 of watchOS gives us developers a lot more control and brings cool features to the users as well. Now that we can download files directly and get access to sensors directly on the watch, the users will benefit.

In this chapter, I am going to assume that you have a simple iOS application in Xcode already created and you want to add a watchOS 2 target to your app. So go to Xcode and create a new Target. On the new window, on the left-hand side, under the watchOS category, choose WatchKit App (see Figure 2-1) and proceed to the next stage.

Figure 2-1. Adding a WatchKit App target to your main application

In the next stage, make sure that you have enabled complications (we’ll talk about it later) and the glance scene (see Figure 2-2).

Figure 2-2. Add a complication and a glance scene to your watch app

After you have created your watch extension, you want to be able to run it on the simulator. To do this, simply choose your app from the targets in Xcode and click the run button (see Figure 2-3).

Figure 2-3. A simple watch interface

2.1 Downloading Files onto the Apple Watch

Problem

You want to be able to download files from your watch app directly without needing to communicate your intentions to the paired iOS device.

Solution

Use NSURLSession as you would on a phone, but with more consideration toward resources and the size of the file you are downloading.

Always consider whether or not you need the file immediately. If you need the file and the size is quite manageable, download it on the watch itself. If the file is big, try to download it on the companion app on the iOS device first and then send the file over to the watch, which itself takes some time.

Discussion

Let’s create an interface similar to Figure 2-4 in our watch extension.

Figure 2-4. Place a label and a button on your interface

Make sure the label can contain at least four lines of text (see Figure 2-5).

Figure 2-5. Lines property must be set to at least 4

Hook up your button’s action to a method in your code named download. Also hook up your label to code under the name statusLbl.

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController, NSURLSessionDelegate,
NSURLSessionDownloadDelegate {

  @IBOutlet var statusLbl: WKInterfaceLabel!

  var status: String = ""{
    didSet{
      dispatch_async(dispatch_get_main_queue()){[unowned self] in
        self.statusLbl.setText(self.status)
      }
    }
  }

  ...
Note

Because NSURLSession delegate methods get called on private queues (not the main thread), I’ve coded a property on our class called status. This is a string property that functions on the private thread can set to indicate what they’re doing, and that is displayed as the text on our label by the main thread.

The most important method of the NSURLSessionDownloadDelegate protocol that we are going to have to implement is the URLSession(_:downloadTask:didFinishDownloadingToURL:) method. It gets called when our file has been downloaded into a URL onto the disk, accessible to the watch. The file there is temporary: when this method returns, the file will be deleted by watchOS. In this method, you can do two things:

  • Read the file directly from the given URL. If you do so, you have to do the reading on a separate thread so that you won’t block NSURLSession’s private queue.
  • Move the file using NSFileManager to another location that is accessible to your extension and then read it later.

We are going to move this file to a location that will later be accessible to our app.

  func URLSession(session: NSURLSession,
    downloadTask: NSURLSessionDownloadTask,
    didFinishDownloadingToURL location: NSURL) {

      let fm = NSFileManager()
      let url = try! fm.URLForDirectory(.DownloadsDirectory,
        inDomain: .UserDomainMask,
        appropriateForURL: location, create: true)
        .URLByAppendingPathComponent("file.txt")

      do{
        try fm.removeItemAtURL(url)
        try fm.moveItemAtURL(location, toURL: url)
        self.status = "Download finished"
      } catch let err{
        self.status = "Error = (err)"
      }

      session.invalidateAndCancel()

  }

The task that we are going to start in order to download the file (you’ll see that soon) will have an identifier. This identifier is quite important for controlling the task after we have started it.

You can see that we also have to call the invalidateAndCancel() method on our task so that we can reuse the same task identifier later. If you don’t do this, the next time you tap the button to redownload the item you won’t be able to.

We are then going to implement a few more useful methods from NSURLSessionDelegate and NSURLSessionDownloadDelegate just so we can show relevant status messages to the user as we are downloading the file:

  func URLSession(session: NSURLSession,
    downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64,
    totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
    status = "Downloaded (bytesWritten) bytes"
  }

  func URLSession(session: NSURLSession,
   downloadTask: NSURLSessionDownloadTask,
    didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
    status = "Resuming the download"
  }

  func URLSession(session: NSURLSession, task: NSURLSessionTask,
    didCompleteWithError error: NSError?) {
      if let e = error{
        status = "Completed with error = (e)"
      } else {
        status = "Finished"
      }
  }

  func URLSession(session: NSURLSession,
    didBecomeInvalidWithError error: NSError?) {
      if let e = error{
        status = "Invalidated (e)"
      } else {
        //no errors occurred, so that's alright
      }
  }

When the user taps the download button, we first define our URL:

    let url = NSURL(string: "http://localhost:8888/file.txt")!
Note

I am running MAMP and hosting my own file called file.txt. This URL won’t get downloaded successfully on your machine if you are not hosting the exact same file with the same name on your local machine on the same port! So I suggest that you change this URL to something that makes more sense for your app.

Then use the backgroundSessionConfigurationWithIdentifier(_:) class method of NSURLSessionConfiguration to create a background URL configuration that you can use with NSURLSession:

    let id = "se.pixolity.app.backgroundtask"
    let conf = NSURLSessionConfiguration
      .backgroundSessionConfigurationWithIdentifier(id)

Once all of that is done, you can go ahead and create a download task and start it (see Figure 2-6):

    let session = NSURLSession(configuration: conf, delegate: self,
      delegateQueue: NSOperationQueue())

    let request = NSURLRequest(URL: url)

    session.downloadTaskWithRequest(request).resume()
Figure 2-6. Our file is successfully downloaded

See Also

Recipe 1.6

2.2 Noticing Changes in Pairing State Between the iOS and Watch Apps

Problem

You want to know, both on the watch and in your companion iOS app, whether there is connectivity between them and whether you can send messages between them. Specifically, you want to find out whether one device can receive a signal sent from the other.

Solution

Import the WatchConnectivity framework on both projects. Then use the WCSession’s delegate of type WCSessionDelegate to implement the sessionWatchStateDidChange(_:) method on your iOS side and the sessionReachabilityDidChange(_:) method on the watch side. These methods get called by WatchConnectivity whenever the state of the companion app is changed (whether that is on the iOS side or on the watchOS side).

Discussion

Both devices contain a flag called reachability that indicates whether the device can connect to the other. This is represented by a property on WCSession called reachable, of type Bool. On the iOS side, if you check this flag, it tells you whether your companion watch app is reachable, and if you check it on the watchOS side, it tells you whether your companion iOS app is reachable.

The idea here is to use the WCSession object to listen for state changes. Before doing that, we need to find out whether the session is actually supported. We do that using the isSupported() class function of WCWCSession. Once you know that sessions are supported, you have to do the following on the iOS app side:

  1. Obtain your session with WCSession.defaultSession().
  2. Set the delegate property of your session.
  3. Become the delegate of your session, of type WCSessionDelegate.
  4. Implement the sessionWatchStateDidChange(_:) function of your session delegate and in there, check the reachable flag of the session.
  5. Call the activateSession() method of your session.

Make sure that you do this in a function that can be called even if your app is launched in the background.

On the watch side, do the exact same things as you did on the iOS side, but instead of implementing the sessionWatchStateDidChange(_:) method, implement the sessionReachabilityDidChange(_:) method.

Note

The sessionWatchStateDidChange(_:) delegate method is called on the iOS side when at least one of the properties of the session changes. These properties include paired, watchAppInstalled, complicationEnabled, and watchDirectoryURL, all of type Bool. In contrast, the sessionReachabilityDidChange(_:) method is called on the watch only when the reachable flag of the companion iOS app is changed, as the name of the delegate method suggests.

So on the iOS side, let’s implement an extension on WCSession that can print all its relevant states, so that when the sessionWatchStateDidChange(_:) method is called, we can print the session’s information:

import UIKit
import WatchConnectivity

extension WCSession{
  public func printInfo(){
    
    //paired
    print("Paired: ", terminator: "")
    print(self.paired ? "Yes" : "No")
    
    //watch app installed
    print("Watch app installed: ", terminator: "")
    print(self.watchAppInstalled ? "Yes" : "No")
    
    //complication enabled
    print("Complication enabled: ", terminator: "")
    print(self.complicationEnabled ? "Yes" : "No")
    
    //watch directory
    print("Watch directory url", terminator: "")
    print(self.watchDirectoryURL)
    
  }
}

Make your app delegate the delegate of the session as well:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate {

  var window: UIWindow?

  ...

Now start listening for state and reachablity changes:

  func sessionReachabilityDidChange(session: WCSession) {
    print("Reachable: ",  terminator: "")
    print(session.reachable ? "Yes" : "No")
  }
  
  func sessionWatchStateDidChange(session: WCSession) {
    print("Watch state is changed")
    session.printInfo()
  }

Last but not least, on the iOS side, set up the session and start listening to its events:

    guard WCSession.isSupported() else {
      print("Session is not supported")
      return
    }

    let session = WCSession.defaultSession()
    session.delegate = self
    session.activateSession()

Now on the watch side, in the ExtensionDelegate class, import WatchConnectivity and become the session delegate as well:

import WatchKit
import WatchConnectivity

class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate {

  ...

And listen for reachablity changes:

  func sessionReachabilityDidChange(session: WCSession) {
    print("Reachablity changed. Reachable?", terminator: "")
    print(session.reachable ? "Yes" : "No")
  }

Then in the applicationDidFinishLaunching() function of our extension delegate, set up the session:

    guard WCSession.isSupported() else {
      print("Session is not supported")
      return
    }

    let session = WCSession.defaultSession()
    session.delegate = self
    session.activateSession()

See Also

Recipe 2.1

2.3 Transferring Small Pieces of Data to and from the Watch

Problem

You want to transfer some plist-serializable content between your apps (iOS and watchOS). This content can be anything: for instance, information about where a user is inside a game on an iOS device, or more random information that you can serialize into a plist (strings, integers, booleans, dictionaries, and arrays). Information can be sent in either direction.

Solution

Follow these steps:

  1. Use what you learned in Recipe 2.2 to find out whether both devices are reachable.
  2. On the sending app, use the updateApplicationContext(_:) method of your session to send the content over to the other app.
  3. On the receiving app, wait for the session(_:didReceiveApplicationContext:) delegate method of WCSessionDelegate, where you will be given access to the transmitted content.
Note

The content that you transmit must be of type [String : AnyObject].

Discussion

Various types of content can be sent between iOS and watchOS. One is plist-serializable content, also called an application context. Let’s say that you are playing a game on watchOS and you want to send the user’s game status to iOS. You can use the application context for this.

Let’s begin by creating a sample application. Create a single-view iOS app and add a watchOS target to it as well (see Figure 2-1). Design your main interface like Figure 2-7. We’ll use the top label to show the download status. The buttons are self-explanatory. The bottom label will show the pairing status between our watchOS and iOS apps.

Figure 2-7. Labels and button for sample app
Note

Hook up the top label to your view controller as statusLbl, the first button as sendBtn, the second button as downloadBtn, and the bottom label as reachabilityStatusLbl. Hook up the action of the download button to a method called download() and the send button to a method called send().

Download and install MAMP (it’s free) and host the following contents as a file called people.json on your local web server’s root folder:

{
  "people" : [
    {
      "name" : "Foo",
      "age" : 30
    },
    {
      "name" : "Bar",
      "age" : 50
    }
  ]
}

Now the top part of your iOS app’s view controller should look like this:

import UIKit
import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate,
NSURLSessionDownloadDelegate {

  @IBOutlet var statusLbl: UILabel!
  @IBOutlet var sendBtn: UIButton!
  @IBOutlet var downloadBtn: UIButton!
  @IBOutlet var reachabilityStatusLbl: UILabel!

  ...

When you download that JSON file, it will become a dictionary of type [String : AnyObject], so let’s define that as a variable in our vc:

  var people: [String : AnyObject]?{
    didSet{
      dispatch_async(dispatch_get_main_queue()){
        self.updateSendButton()
      }
    }
  }

  func updateSendButton(){
    sendBtn.enabled = isReachable && isDownloadFinished && people != nil
  }
Note

Setting the value of the people variable will call the updateSendButton() function, which in turn enables the send button only if all the following conditions are met:

  1. The watch app is reachable.

  2. The file is downloaded.

  3. The file was correctly parsed into the people variable.

Also define a variable that can write into your status label whenever the reachability flag is changed:

  var isReachable = false{
    didSet{
      dispatch_async(dispatch_get_main_queue()){
        self.updateSendButton()
        if self.isReachable{
          self.reachabilityStatusLbl.text = "Watch is reachable"
        } else {
          self.reachabilityStatusLbl.text = "Watch is not reachable"
        }
      }
    }
  }

We need two more properties: one that sets the status label and another that keeps track of when our file is downloaded successfully:

  var isDownloadFinished = false{
    didSet{
      dispatch_async(dispatch_get_main_queue()){
        self.updateSendButton()
      }
    }
  }

  var status: String?{
    get{return self.statusLbl.text}
    set{
      dispatch_async(dispatch_get_main_queue()){
        self.statusLbl.text = newValue
      }
    }
  }
Note

All three variables (people, isReachable, and isDownloadFinished) that we defined call the updateSendButton() function so that our send button will be disabled if conditions are not met, and enabled otherwise.

Now when the download button is pressed, start a download task:

  @IBAction func download() {

    //if loading HTTP content, make sure you have disabled ATS
    //for that domain
    let url = NSURL(string: "http://localhost:8888/people.json")!
    let req = NSURLRequest(URL: url)
    let id = "se.pixolity.app.backgroundtask"

    let conf = NSURLSessionConfiguration
      .backgroundSessionConfigurationWithIdentifier(id)

    let sess = NSURLSession(configuration: conf, delegate: self,
      delegateQueue: NSOperationQueue())

    sess.downloadTaskWithRequest(req).resume()
  }

After that, check if you got any errors while trying to download the file:

  func URLSession(session: NSURLSession, task: NSURLSessionTask,
    didCompleteWithError error: NSError?) {

      if error != nil{
        status = "Error happened"
        isDownloadFinished = false
      }

      session.finishTasksAndInvalidate()

  }

Now implement the URLSession(_:downloadTask:didFinishDownloadingToURL:) method of NSURLSessionDownloadDelegate. Inside there, tell your view controller that you have downloaded the file by setting isDownloadFinished to true. Then construct a more permanent URL for the temporary URL to which our JSON file was downloaded by iOS:

  func URLSession(session: NSURLSession,
    downloadTask: NSURLSessionDownloadTask,
    didFinishDownloadingToURL location: NSURL){

      isDownloadFinished = true

      //got the data, parse as JSON
      let fm = NSFileManager()
      let url = try! fm.URLForDirectory(.DownloadsDirectory,
        inDomain: .UserDomainMask,
        appropriateForURL: location,
        create: true).URLByAppendingPathComponent("file.json")

      ...

Then move the file over:

      do {try fm.removeItemAtURL(url)} catch {}

      do{
        try fm.moveItemAtURL(location, toURL: url)
      } catch {
        status = "Could not save the file"
        return
      }

After that, simply read the file as a JSON file with NSJSONSerialization:

      //now read the file from url
      guard let data = NSData(contentsOfURL: url) else{
        status = "Could not read the file"
        return
      }

      do{
        let json = try NSJSONSerialization.JSONObjectWithData(data,
        options: .AllowFragments) as! [String : AnyObject]
        self.people = json
        status = "Successfully downloaded and parsed the file"
      } catch{
        status = "Could not read the file as json"
      }

Great—now go to your watch interface, place a label there, and hook it up to your code under the name statusLabel (see Figure 2-8).

In the interface controller file, place a variable that can set the status:

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController {

  @IBOutlet var statusLabel: WKInterfaceLabel!

  var status = "Waiting"{
    didSet{
      statusLabel.setText(status)
    }
  }

}
Figure 2-8. Our watch interface has a simple label only

Go to your ExtensionDelegate file on the watch side and do these things:

  1. Define a structure that can hold instances of a person you will get in your application context.
  2. Define a property called status that when set, will set the status property of the interface controller:
import WatchKit
import WatchConnectivity

struct Person{
  let name: String
  let age: Int
}

class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate{

  var status = ""{
    didSet{
      dispatch_async(dispatch_get_main_queue()){
        guard let interface =
          WKExtension.sharedExtension().rootInterfaceController as?
          InterfaceController else{
          return
        }
        interface.status = self.status
      }
    }
  }


  ...

Now activate the session using what you learned in Recipe 2.2. I won’t write the code for that in this recipe again. Then the session will wait for the session(_:didReceiveApplicationContext:) method of the WCSessionDelegate protocol to come in. When that happens, just read the application context and convert it into Person instances:

  func session(session: WCSession,
    didReceiveApplicationContext applicationContext: [String : AnyObject]) {

      guard let people = applicationContext["people"] as?
        Array<[String : AnyObject]> where people.count > 0 else{
          status = "Did not find the people array"
        return
      }

      var persons = [Person]()
      for p in people where p["name"] is String && p["age"] is Int{
        let person = Person(name: p["name"] as! String, age: p["age"] as! Int)
        persons.append(person)
      }

      status = "Received (persons.count) people from the iOS app"

  }

Now run both your watch app and your iOS app. At first glance, your watch app will look like Figure 2-9.

Figure 2-9. Your watch app is waiting for the context to come through from the iOS app

Your iOS app in its initial state will look like Figure 2-10.

Figure 2-10. Your iOS app has detected that its companion watch app is reachable

When I press the download button, my iOS app’s interface will change to Figure 2-11.

Figure 2-11. The iOS app is now ready to send the data over to the watch app

After pressing the send button, the watch app’s interface will change to something like Figure 2-12.

Figure 2-12. The watch app received the data

See Also

Recipe 2.1

2.4 Transferring Dictionaries in Queues to and from the Watch

Problem

You want to send dictionaries of information to and from the watch in a queuing (FIFO) fashion.

Solution

Call the transferUserInfo(_:) method on your WCSession on the sending part. On the receiving part, implement the session(_:didReceiveUserInfo:) method of the WCSessionDelegate protocol.

Note

A lot of the things that I’ll refer to in this recipe have been discussed already in Recipe 2.3, so have a look if you feel a bit confused.

Discussion

Create a single-view app in iOS and put your root view controller in a nav controller. Then add a watch target to your app (see this chapter’s introduction for an explanation). Make sure that your root view controller in IB looks like Figure 2-13.

Figure 2-13. Place a label and a button on your UI

Hook up the label to a variable in your code named statusLbl and hook up the button to a variable named sendBtn. Hook up your button’s action to a method in your code called send(). The top of your vc should now look like:

import UIKit
import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate {

  @IBOutlet var statusLbl: UILabel!
  @IBOutlet var sendBtn: UIButton!

  ...

You also need a property that can set the status for you on your label. The property must be on the main thread, because WCSession methods (where we may want to set our status property) usually are not called on the main thread:

  var status: String?{
    get{return self.statusLbl.text}
    set{
      dispatch_async(dispatch_get_main_queue()){
        self.statusLbl.text = newValue
      }
    }
  }

When the user presses the send button, we will use the WCSession.defaultSession().transferUserInfo(_:) method to send a simple dictionary whose only key is kCFBundleIdentifierKey and a value that will be our Info.plist’s bundle identifier:

  @IBAction func send() {

    guard let infoPlist = NSBundle.mainBundle().infoDictionary else{
      status = "Could not get the Info.plist"
      return
    }

    let key = kCFBundleIdentifierKey as String

    let plist = [
      key : infoPlist[key] as! String
    ]

    let transfer = WCSession.defaultSession().transferUserInfo(plist)
    status = transfer.transferring ? "Sent" : "Could not send yet"

  }

The transferUserInfo(_:) method returns an object of type WCSessionUserInfoTransfer that has properties such as userInfo and transferring and a method called cancel(). You can always use the cancel() method of an instance of WCSessionUserInfoTransfer to cancel the transfer of this item if it is not already transferring. You can also find all the user info transfers that are ongoing by using the outstandingUserInfoTransfers property of your session object.

Note

The app also contains code to disable the button if the watch app is not reachable, but I won’t discuss that code here because we have already discussed it in Recipe 2.2 and Recipe 2.3.

On the watch side, in InterfaceController, write the exact same code that you wrote in Recipe 2.3. In the ExtensionDelegate class, however, our code will be a bit different. Its status property is exactly how we wrote it in Recipe 2.3.

When the applicationDidFinishLaunching() method of our delegate is called, we set up the session just as we did in Recipe 2.2. We will wait for the session(_:didReceiveUserInfo:) method of the WCSessionDelegate protocol to be called. There, we will simply read the bundle identifier from the user info and display it in our view controller:

  func session(session: WCSession,
    didReceiveUserInfo userInfo: [String : AnyObject]) {

      guard let bundleVersion = userInfo[kCFBundleIdentifierKey as String]
        as? String else{
        status = "Could not read the bundle version"
        return
      }

      status = bundleVersion

  }

If you run the iOS app, your UI should look like Figure 2-14.

Figure 2-14. The app has detected that the watch app is reachable so the button is enabled

And your watch app should look like Figure 2-15.

Figure 2-15. The watch app is waiting for incoming user info data

When you press the send button, the user interface will change to Figure 2-16.

Figure 2-16. The data is sent to the watch

And the watch app will look like Figure 2-17.

Figure 2-17. The watch app successfully received our user info

See Also

Recipe 2.1 and Recipe 2.3

2.5 Transferring Files to and from the Watch

Problem

You want to transfer a file between your iOS app and the watch app. The technique works in both directions.

Solution

Follow these steps:

  1. Use the transferFile(_:metadata:) method of your WCSession object on the sending device.
  2. Implement the WCSessionDelegate protocol on the sender and wait for the session(_:didFinishFileTransfer:error:) delegate method to be called. If the optional error parameter is nil, it indicates that the file is transferred successfully.
  3. On the receiving part, become the delegate of WCSession and wait for the session(_:didReceiveFile:) delegate method to be called.
  4. The incoming file on the receiving side is of type WCSessionFile and has properties such as fileURL and metadata. The metadata is the same metadata of type [String : AnyObject] that the sender sent with the transferFile(_:metadata:) method.

Discussion

Let’s have a look at a simple UI on the sending device (the iOS side in this example). It contains a label that shows our status and a button that sends our file. When the button is pressed, we create a file in the iOS app’s caches folder and then send that file through to the watch app if it is reachable (see Recipe 2.2).

Make your UI on the iOS (sender) side look like Figure 2-18. The button will be disabled if the watch app is not reachable (see Recipe 2.2).

Figure 2-18. Status label and button on sender

Hook up your button’s action code to a method in your view controller called send() and make sure your view controller conforms to WCSessionDelegate:

import UIKit
import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate {

  @IBOutlet var statusLbl: UILabel!
  @IBOutlet var sendBtn: UIButton!

  var status: String?{
    get{return self.statusLbl.text}
    set{
      dispatch_async(dispatch_get_main_queue()){
        self.statusLbl.text = newValue
      }
    }
  }

  ...
Note

We implemented and talked about the status property of our view controller in Recipe 2.3, so I won’t explain it here.

Then, when the send button is pressed, construct a URL that will point to your file. It doesn’t exist yet, but you will write it to disk soon:

    let fileName = "file.txt"

    let fm = NSFileManager()

    let url = try! fm.URLForDirectory(.CachesDirectory,
      inDomain: .UserDomainMask, appropriateForURL: nil,
      create: true).URLByAppendingPathComponent(fileName)

Now write some text to disk, reachable through the URL:

    let text = "Foo Bar"

    do{
      try text.writeToURL(url, atomically: true,
        encoding: NSUTF8StringEncoding)
    } catch {
      status = "Could not write the file"
      return
    }

Once that is done, send the file over:

    let metadata = ["fileName" : fileName]
    WCSession.defaultSession().transferFile(url, metadata: metadata)

Also, when your session’s reachability state changes, enable or disable your button:

  func updateUiForSession(session: WCSession){
    status = session.reachable ? "Ready to send" : "Not reachable"
    sendBtn.enabled = session.reachable
  }

  func sessionReachabilityDidChange(session: WCSession) {
    updateUiForSession(session)
  }

On the watch side, make your UI look like Figure 2-8. Then, in your ExtensionDelegate class, implement the exact same status property that we implemented in Recipe 2.3.

Now implement the session(_:didReceiveFile:) method of WCSessionDelegate. Start by double-checking that the metadata is as you expected it:

    guard let metadata = file.metadata where metadata["fileName"]
      is String else{
      status = "No metadata came through"
      return
    }

If it is, read the file and show it in the user interface:

    do{
      let str = try String(NSString(contentsOfURL: file.fileURL,
        encoding: NSUTF8StringEncoding))
      guard str.characters.count > 0 else{
        status = "No file came through"
        return
      }
      status = str
    } catch {
      status = "Could not read the file"
      return
    }

When you run the watch app, it will look like Figure 2-15. When you run the iOS app, it will look like Figure 2-19.

Figure 2-19. The file is ready to be sent from iOS to watchOS

When the file is sent, your user interface on iOS will look like Figure 2-20.

Figure 2-20. iOS sent our file to watchOS

And the UI on your receiver (watchOS) will look like Figure 2-21.

Figure 2-21. watchOS successfully received our file, read its content, and is displaying it in our label

2.6 Communicating Interactively Between iOS and watchOS

Problem

You want to interactively send messages from iOS to watchOS (or vice versa) and receive a reply immediately.

Solution

On the sender side, use the sendMessage(_:replyHandler:errorHandler:) method of WCSession. On the receiving side, implement the session(_:didReceiveMessage:replyHandler:) method to handle the incoming message if your sender expected a reply, or implement session(_:didReceiveMessage:) if no reply was expected from you. Messages and replies are of type [String : AnyObject].

Discussion

Let’s implement a chat program where the iOS app and the watch app can send messages to each other. On the iOS app, we will allow the user to type text and then send it over to the watch. On the watch, since we cannot type anything, we will have four predefined messages that the user can send us. In order to decrease the amount of data the watch sends us, we define these messages as Int and send the integers instead. The iOS app will read the integers and then print the correct message onto the screen. So let’s first define these messages. Create a file called PredefinedMessages and write the following Swift code there:

import Foundation

enum PredefinedMessage : Int{
  case Hello
  case ThankYou
  case HowAreYou
  case IHearYou
}

Add this file to both your watch extension and your iOS app so that they both can use it (see Figure 2-22).

Figure 2-22. We will include the file on our iOS app and watch extension

Now move to your main iOS app’s storyboard and design a UI that looks like Figure 2-23. There are two labels that say “...” at the moment. They will be populated dynamically in our code.

Figure 2-23. Initial iOS app UI

Hook up your UI to your code as follows:

  • Hook up your send button to an outlet called sendBtn. Hook up its action method to a function called send(_:) in your vc.
  • Hook up the text field to your code under the name textField.
  • Hook up the label that says “...” in front of “Watch Status:” to an outlet called watchStatusLbl.
  • Hook up the label that says “...” in front of “Watch Said:” to an outlet called watchReplyLbl.

So now the top part of your vc on the iOS side should look like this:

import UIKit
import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate {

  @IBOutlet var sendBtn: UIBarButtonItem!
  @IBOutlet var textField: UITextField!
  @IBOutlet var watchStatusLbl: UILabel!
  @IBOutlet var watchReplyLbl: UILabel!


  ...

As we have done before, we need two variables that can populate the text inside the watchStatusLbl and watchReplyLbl labels, always on the main thread:

  var watchStatus: String{
    get{return self.watchStatusLbl.text ?? ""}
    set{onMainThread{self.watchStatusLbl.text = newValue}}
  }

  var watchReply: String{
    get{return self.watchReplyLbl.text ?? ""}
    set{onMainThread{self.watchReplyLbl.text = newValue}}
  }
Note

The definition of onMainThread is very simple. It’s a custom function I’ve written in a library to make life easier:

import Foundation

public func onMainThread(f: () -> Void){
  dispatch_async(dispatch_get_main_queue(), f)
}

When the send button is pressed, we first have to make sure that the user has entered some text into the text field:

    guard let txt = textField.text where txt.characters.count > 0 else{
      textField.placeholder = "Enter some text here first"
      return
    }

Then we will use the sendMessage(_:replyHandler:errorHandler:) method of our session to send our text over:

    WCSession.defaultSession().sendMessage(["msg" : txt],
      replyHandler: {dict in

        guard dict["msg"] is String &&
          dict["msg"] as! String == "delivered" else{
          self.watchReply = "Could not deliver the message"
          return
        }

        self.watchReply = dict["msg"] as! String

    }){err in
      self.watchReply = "An error happened in sending the message"
    }

Later, when we implement our watch side, we will also be sending messages from the watch over to the iOS app. Those messages will be inside a dictionary whose only key is “msg” and the value of this key will be an integer. The integers are already defined in the PredefinedMessage enum that we saw earlier. So in our iOS app, we will wait for messages from the watch app, translate the integer we get to its string counterpart, and show it on our iOS UI. Remember, we send integers (instead of strings) from the watch to make the transfer snappier. So let’s implement the session(_:didReceiveMessage:) delegate method in our iOS app:

  func session(session: WCSession,
    didReceiveMessage message: [String : AnyObject]) {

      guard let msg = message["msg"] as? Int,
        let value = PredefinedMessage(rawValue: msg) else{
          watchReply = "Received invalid message"
        return
      }

      switch value{
      case .Hello:
        watchReply = "Hello"
      case .HowAreYou:
        watchReply = "How are you?"
      case .IHearYou:
        watchReply = "I hear you"
      case .ThankYou:
        watchReply = "Thank you"
      }

  }

Let’s use what we learned in Recipe 2.2 to enable or disable our send button when the watch’s reachability changes:

  func updateUiForSession(session: WCSession){
    watchStatus = session.reachable ? "Reachable" : "Not reachable"
    sendBtn.enabled = session.reachable
  }

  func sessionReachabilityDidChange(session: WCSession) {
    updateUiForSession(session)
  }

On the watch side, design your UI like Figure 2-24. On the watch, the user cannot type, but she can press a predefined message in order to send it (remember PredefinedMessage?). That little line between “Waiting...” and “Send a reply” is a separator.

Figure 2-24. Strings that a user can send from a watch

Hook up your watch UI to your code by following these steps:

  • Hook up the “Waiting...” label to an outlet named iosAppReplyLbl. We will show the text that our iOS app has sent to us in this label.
  • Place all the buttons at the bottom of the page inside a group and hook that group up to an outlet called repliesGroup. We will hide this whole group if the iOS app is not reachable to our watch app.
  • Hook the action of the “Hello” button to a method in your code called sendHello().
  • Hook the action of the “Thank you” button to a method in your code called sendThankYou().
  • Hook the action of the “How are you?” button to a method in your code called sendHowAreYou().
  • Hook the action of the “I hear you” button to a method in your code called sendIHearYou().

In our InterfaceController on the watch side, we need a generic method that takes in an Int (our predefined message) and sends it over to the iOS side with the sendMessage(_:replyHandler:errorHandler:) method of the session:

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController {

  @IBOutlet var iosAppReplyLbl: WKInterfaceLabel!
  @IBOutlet var repliesGroup: WKInterfaceGroup!

  func send(int: Int){

    WCSession.defaultSession().sendMessage(["msg" : int],
      replyHandler: nil, errorHandler: nil)

  }

  ...

And whenever any of the buttons is pressed, we call the send(_:) method with the right predefined message:

  @IBAction func sendHello() {
    send(PredefinedMessage.Hello.hashValue)
  }

  @IBAction func sendThankYou() {
    send(PredefinedMessage.ThankYou.hashValue)
  }

  @IBAction func sendHowAreYou() {
    send(PredefinedMessage.HowAreYou.hashValue)
  }

  @IBAction func sendIHearYou() {
    send(PredefinedMessage.IHearYou.hashValue)
  }

In the ExtensionDelegate class on the watch side, we want to hide all the reply buttons if the iOS app is not reachable. To do that, write a property called isReachable of type Bool. Whenever this property is set, the code sets the hidden property of our replies group:

import WatchKit
import WatchConnectivity

class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate{

  var isReachable = false{
    willSet{
      self.rootController?.repliesGroup.setHidden(!newValue)
    }
  }

  var rootController: InterfaceController?{
    get{
      guard let interface =
        WKExtension.sharedExtension().rootInterfaceController as?
        InterfaceController else{
          return nil
      }
      return interface
    }
  }

  ...

You also are going to need a String property that will be your iOS app’s reply. Whenever you get a reply from the iOS app, place it inside this property. As soon as this property is set, the watch extension will write this text on our UI:

  var iosAppReply = ""{
    didSet{
      dispatch_async(dispatch_get_main_queue()){
        self.rootController?.iosAppReplyLbl.setText(self.iosAppReply)
      }
    }
  }

Now let’s wait for messages from the iOS app and display those messages on our UI:

  func session(session: WCSession,
    didReceiveMessage message: [String : AnyObject],
    replyHandler: ([String : AnyObject]) -> Void) {

      guard message["msg"] is String else{
        replyHandler(["msg" : "failed"])
        return
      }

      iosAppReply = message["msg"] as! String
      replyHandler(["msg" : "delivered"])

  }

Also when our iOS app’s reachability changes, we want to update our UI and disable the reply buttons:

  func sessionReachabilityDidChange(session: WCSession) {
    isReachable = session.reachable
  }

  func applicationDidFinishLaunching() {

    guard WCSession.isSupported() else{
      iosAppReply = "Sessions are not supported"
      return
    }

    let session = WCSession.defaultSession()
    session.delegate = self
    session.activateSession()
    isReachable = session.reachable

  }

Running our app on the watch first, we will see an interface similar to Figure 2-25. The user can scroll to see the rest of the buttons.

Figure 2-25. Available messages on watch

And when we run our app on iOS while the watch app is reachable, the UI will look like Figure 2-26.

Figure 2-26. The send button on our app is enabled and we can send messages

Type “Hello from iOS” in the iOS UI and press the send button. The watch app will receive the message (see Figure 2-27).

Figure 2-27. The watch app received the message sent from the iOS app

Now press the How are you? button on the watch UI and see the results in the iOS app (Figure 2-28).

Figure 2-28. The iOS app received the message from the watch app

2.7 Setting Up Apple Watch for Custom Complications

Problem

You want to create a barebones watch project with support for complications and you want to see a complication on the screen.

Solution

Follow these steps:

  1. Add a watch target to your project (see Figure 2-1). Make sure that it includes complications upon setting it up.
  2. In Xcode, in your targets, select your watch extension. Under the General tab, ensure that the Modular Small complication is the only complication that is enabled. Disable all the others (see Figure 2-29).
  3. Write your complication code in your ComplicationController class. We’ll discuss this code soon.
  4. Run your app on the watch simulator.
  5. Once your app is opened in the simulator, press Command-Shift-H to go to the clock face (see Figure 2-3).
  6. Press Command-Shift-2 to simulate Deep Press on the watch simulator and then tap and hold on the watch face (see Figure 2-30).
Figure 2-29. We are going to support only small-modular complications
Figure 2-30. We can now customize our watch face
  1. Press Command-Shift-1 to simulate Shallow Press and then scroll to the modular watch face (see Figure 2-31).
Figure 2-31. Select the modular watch face
  1. Press the Customize button (see Figure 2-32).
Figure 2-32. Now you can customize your modular watch face
  1. Scroll to the next page to the right, and then tap the small-modular complication at the bottom left of the screen until it becomes selected (see Figure 2-33). You will replace this with your own complication.
Figure 2-33. Select the small modular complication at the bottom left
  1. Now use the up and down arrows on your keyboard (or if on the device, use the digital crown) to select your complication (see Figure 2-34). What you see on the screen is the preview template that you have provided to the system. We will implement this template soon, but in the figure I have already done that, hence the number 22.
Figure 2-34. Your own small-modular complication is shown
  1. Press Cmd-Shift-2 to simulate Deep Press and then tap the screen (see Figure 2-35).
Figure 2-35. We have now configured our complication on the selected watch face
  1. Press Command-Shift-H to go to the clock app on the screen (see Figure 2-36). Notice that your complication is gone and shows no data. That is because what we displayed on the screen while configuring our watch face was just a preview template. What the clock app displays is real data and we are not providing any of it.
Figure 2-36. Our complication is on the bottom left but is empty

Discussion

Complications are pieces of information that apps can display on a watch face. They are divided into a few main categories:

Modular small
A very small amount of space with minimal text and/or a very small image (see Figure 2-37; the date on the top left is a modular small complication).
Modular large
An image, title, and up to two lines of text (see Figure 2-37; the calendar event in the center of the screen is a modular large complication).
Utilitarian small
Mainly a small image with optional text (see Figure 2-37; the activity icon in the bottom center is of this type).
Utilitarian large
A date/text mixed with an image, rendered on one line. This is similar to modular large but on just one line.
Circular small
A circular image with optional text (see Figure 2-37; the sunrise/sunset complication on the bottom right is an example of a circular-small complication).
Figure 2-37. Everything except the time is a complication

Assuming that you have already created a watch target with a complication attached to it, go into your ComplicationController class and find the getPlaceholderTemplateForComplication(_:withHandler:) method. This method gets called by iOS when your complication is being added to a watch face. This gives you the chance to provide a placeholder for what the user has to see while adjusting her watch face. It won’t usually be real data.

After this method is called, you need to create a complication template of type CLKComplicationTemplate (or one of its many subclasses) and return that into the replyHandler block that you are given. For now, implement the template like this:

  func getPlaceholderTemplateForComplication(complication: CLKComplication,
    withHandler handler: (CLKComplicationTemplate?) -> Void) {
    let temp = CLKComplicationTemplateModularSmallSimpleText()
    temp.textProvider = CLKSimpleTextProvider(text: "22")
    handler(temp)
  }
Note

I am not going to discuss the details of this code right now. You’ll learn them in other recipes in this chapter.

One more thing that you have to know is that once you have provided watchOS with your placeholder template, you won’t be asked to do it again unless the user uninstalls your watchOS app and installs it again from her iPhone (see Figure 2-38).

Figure 2-38. If the user uninstalls and reinstalls your app, it can provide a new placeholder template

If you are working on the getPlaceholderTemplateForComplication(_:withHandler:) method and want to test out different templates, you can simply reset the watch simulator and then run your app again. This will retrigger the getPlaceholderTemplateForComplication(_:withHandler:) method on your complication controller.

See Also

Recipe 2.8 and Recipe 2.9

2.8 Constructing Small Complications with Text and Images

Problem

You want to construct a small-modular complication and provide the user with past, present, and future data. In this example, a small modular complication (Figure 2-39, bottom left) shows the current hour with a ring swallowing it. The ring is divided into 24 sections and increments for every 1 hour in the day. At the end of the day, the ring will be completely filled and the number inside the ring will show 24.

Figure 2-39. Small-modular complication (bottom left) showing the current hour surrounded by a ring

Solution

Follow these steps:

  1. Create your main iOS project with a watch target and make sure your watch target has a complication.
  2. In your complication, implement the getSupportedTimeTravelDirectionsForComplication(_:withHandler:) method of the CLKComplicationDataSource protocol. In this method, return your supported time travel directions (more on this later). The directions are of type CLKComplicationTimeTravelDirections.
  3. Implement the getTimelineStartDateForComplication(_:withHandler:) method inside your complication class and call the given handler with an NSDate that indicates the start date of your available data.
  4. Implement the getTimelineEndDateForComplication(_:withHandler:) method of your complication and call the handler with the last date for which your data is valid.
  5. Implement the getTimelineEntriesForComplication(_:beforeDate:limit:withHandler:) method of your complication, create an array of type CLKComplicationTimelineEntry, and send that array into the given handler object. These will be the timeline entries before the given date that you would want to return to the watch (more on this later).
  6. Implement the getTimelineEntriesForComplication(_:afterDate:limit:withHandler:) method of your complication and return all the events that your complication supports, after the given date.
  7. Implement the getNextRequestedUpdateDateWithHandler(_:) method of your complication and let watchOS know when it has to ask you next for more content.

Discussion

When providing complications, you are expected to provide data to the watchOS as the time changes. In our example, for every hour in the day, we want to change our complication. So each day we’ll return 24 events to the runtime.

With the digital crown on the watch, the user can scroll up and down while on the watch face to engage in a feature called “time travel.” This allows the user to change the time known to the watch just so she can see how various components on screen change with the new time. For instance, if you provide a complication to the user that shows all football match results of the day, the user can then go back in time a few hours to see the results of a match she has just missed. Similarly, in the context of a complication that shows the next fast train time to the city where the user lives, she can scroll forward, with the digital crown on the watch face, to see the future times that the train leaves from the current station.

The time is an absolute value on any watch, so let’s say that you want to provide the time of the next football match in your complication. Let’s say it’s 14:00 right now and the football match starts at 15:00. If you give 15:00 as the start of that event to your complication, watchOS will show the football match (or the data that you provide for that match to your user through your complication) to the user at 15:00, not before. That is a bit useless, if you ask me. You want to provide that information to the user before the match starts so she knows what to look forward to, and when. So keep that in mind when providing a starting date for your events.

watchOS complications conform to the CLKComplicationDataSource protocol. They get a lot of delegate messages from this protocol calling methods that you have to implement even if you don’t want to return any data. For instance, in the getNextRequestedUpdateDateWithHandler(_:) method, you get a handler as a parameter that you must call with an NSDate object, specifying when you want to be asked for more data next time. If you don’t want to be asked for any more data, you still have to call this handler object but with a nil date. You’ll find out soon that most of these handlers ask for optional values, so you can call them with nil if you want to.

While working with complications, you can tell watchOS which directions of time travel you support, or if you support time travel at all. If you don’t support it, your complication returns only data for the current time. And if the user scrolls the watch face with the digital crown, your complication won’t update its information. I don’t suggest you opt out of time travel unless your complication really cannot provide relevant data to the user. Certainly, if your complication shows match results, it cannot show results for matches that have not happened. But even then, you can still support forward and backward time travel. If the user chooses forward time travel, just hide the scores, show a question mark, or do something similar.

As you work with complications, it’s important to construct a data model to return to the watch. What you usually return to the watch for your complication is either of type CLKComplicationTemplate or of type CLKComplicationTimelineEntry. The template defines how your data is viewed on screen. The timeline entry only binds your template (your visible data) to a date of type NSDate that dictates to the watch when it has to show your data. As simple as that. In the case of small-modular complications, you can provide the following templates to the watch:

CLKComplicationTemplateModularSmallSimpleText
Has just text.
CLKComplicationTemplateModularSmallSimpleImage
Has just an image.
CLKComplicationTemplateModularSmallRingText
Has text inside a ring that you can fill from 0 to 100%.
CLKComplicationTemplateModularSmallRingImage
Has an image inside a ring that you can fill.
CLKComplicationTemplateModularSmallStackText
Has two lines of code, the second of which can be highlighted.
CLKComplicationTemplateModularSmallStackImage
Has an image and a text, with the text able to be highlighted.
CLKComplicationTemplateModularSmallColumnsText
Has a 2 × 2 text display where you can provide four pieces of textual data. The second column can be highlighted and have its text alignment adjusted.

As you saw in Figure 2-33, this example bases our small-modular template on CLKComplicationTemplateModularSmallRingText. So we provide only a text (the current hour) and a value between 0 and 1 that will tell watchOS how much of the ring around our number it has to fill (0...100%).

Let’s now begin defining our data for this example. For every hour, we want our template to show the current hour. Just before midnight, we provide another 24 new complication data points for that day to the watch. So let’s define a data structure that can contain a date, the hour value, and the fraction (between 0...1) to set for our complication. Start off by creating a file called DataProvider.swift and write all this code in that:

protocol WithDate{
  var hour: Int {get}
  var date: NSDate {get}
  var fraction: Float {get}
}

Now we can define our actual structure that conforms to this protocol:

struct Data : WithDate{
  let hour: Int
  let date: NSDate
  let fraction: Float
  var hourAsStr: String{
    return "(hour)"
  }
}

Later, when we work on our complication, we will be asked to provide, inside the getCurrentTimelineEntryForComplication(_:withHandler:) method of CLKComplicationDataSource, a template to show to the user for the current time. We are also going to create an array of 24 Data structures. So it would be great if we could always, inside this array, easily find the Data object for the current date:

extension NSDate{
  func hour() -> Int{
    let cal = NSCalendar.currentCalendar()
    return cal.components(NSCalendarUnit.Hour, fromDate: self).hour
  }
}

extension CollectionType where Generator.Element : WithDate {

  func dataForNow() -> Generator.Element?{
    let thisHour = NSDate().hour()
    for d in self{
      if d.hour == thisHour{
        return d
      }
    }
    return nil
  }

}
Note

The dataForNow() function goes through any collection that has objects that conform to the WithDate protocol that we specified earlier, and finds the object whose current hour is the same as that returned for the current moment by NSDate().

Let’s now create our array of 24 Data objects. We do this by iterating from 1 to 24, creating NSDate objects using NSDateComponents and NSCalendar. Then, using those objects, we construct instances of the Data structure that we just wrote:

struct DataProvider{

  func allDataForToday() -> [Data]{

    var all = [Data]()

    let now = NSDate()
    let cal = NSCalendar.currentCalendar()
    let units = NSCalendarUnit.Year.union(.Month).union(.Day)
    let comps = cal.components(units, fromDate: now)
    comps.minute = 0
    comps.second = 0
    for i in 1...24{
      comps.hour = i
      let date = cal.dateFromComponents(comps)!
      let fraction = Float(comps.hour) / 24.0
      let data = Data(hour: comps.hour, date: date, fraction: fraction)
      all.append(data)
    }

    return all

  }

}

That was our entire data model. Now let’s move onto the complication class of our watch app. In the getNextRequestedUpdateDateWithHandler(_:) method of the CLKComplicationDataSource protocol to which our complication conforms, we are going to be asked when watchOS should next call our complication and ask for new data. Because we are going to provide data for the whole day, today, we would want to be asked for new data for tomorrow. So we need to ask to be updated a few seconds before the start of the next day. For that, we need an NSDate object that tells watchOS when the next day is. So let’s extend NSDate:

extension NSDate{

  class func endOfToday() -> NSDate{
    let cal = NSCalendar.currentCalendar()
    let units = NSCalendarUnit.Year.union(NSCalendarUnit.Month)
      .union(NSCalendarUnit.Day)
    let comps = cal.components(units, fromDate: NSDate())
    comps.hour = 23
    comps.minute = 59
    comps.second = 59
    return cal.dateFromComponents(comps)!
  }

}

Moving to our complication, let’s define our data provider first:

class ComplicationController: NSObject, CLKComplicationDataSource {

  let dataProvider = DataProvider()

  ...

We know that our data provider can give us an array of Data objects, so we need a way of turning those objects into our templates so they that can be displayed on the screen:

  func templateForData(data: Data) -> CLKComplicationTemplate{
    let template = CLKComplicationTemplateModularSmallRingText()
    template.textProvider = CLKSimpleTextProvider(text: data.hourAsStr)
    template.fillFraction = data.fraction
    template.ringStyle = .Closed
    return template
  }

Our template of type CLKComplicationTemplateModularSmallRingText has a few important properties:

textProvider of type CLKTextProvider
Tells watchOS how our text has to appear. We never instantiate CLKTextProvider directly, though. We use one of its subclasses, such as the CLKSimpleTextProvider class. There are other text providers that we will talk about later.
fillFraction of type Float
A number between 0.0 and 1.0 that tells watchOS how much of the ring around our template it has to fill.
ringStyle of type CLKComplicationRingStyle
The style of the ring we want around our text. It can be Open or Closed.

Later we are also going to be asked for timeline entries of type CLKComplicationTimelineEntry for the data that we provide to watchOS. So for every Data object, we need to be able to create a timeline entry:

  func timelineEntryForData(data: Data) -> CLKComplicationTimelineEntry{
    let template = templateForData(data)
    return CLKComplicationTimelineEntry(date: data.date,
      complicationTemplate: template)
  }

In this example, we support forward and backward time travel (of type CLKComplicationTimeTravelDirections) so let’s tell watchOS that:

  func getSupportedTimeTravelDirectionsForComplication(
    complication: CLKComplication,
    withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {
      handler([.Forward, .Backward])
  }
Note

If you don’t want to support time travel, call the handler argument with the value of CLKComplicationTimeTravelDirections.None.

The next thing we have to do is implement the getTimelineStartDateForComplication(_:withHandler:) method of CLKComplicationDataSource. This method gets called on our delegate whenever watchOS wants to find out the beginning of the date/time range of our time travel. For our example, since we want to provide 24 templates, one for each hour in the day, we tell watchOS the date of the first template:

  func getTimelineStartDateForComplication(complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {
      handler(dataProvider.allDataForToday().first!.date)
  }

Similarly, for the getTimelineEndDateForComplication(_:withHandler:) method, we provide the date of the last event:

  func getTimelineEndDateForComplication(complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {
    handler(dataProvider.allDataForToday().last!.date)
  }

Complications can be displayed on the watch’s lock screen. Some complications might contain sensitive data, so they might want to opt out of appearing on the lock screen. For this, we have to implement the getPrivacyBehaviorForComplication(_:withHandler:) method as well. We call the handler with an object of type CLKComplicationPrivacyBehavior, such as ShowOnLockScreen or HideOnLockScreen. Because we don’t have any sensitive data, we show our complication on the lock screen:

  func getPrivacyBehaviorForComplication(complication: CLKComplication,
    withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {
    handler(.ShowOnLockScreen)
  }

Now to the stuff that I like. The getCurrentTimelineEntryForComplication(_:withHandler:) method will get called on our delegate whenever the runtime needs to get the complication timeline (the template plus the date to display) for the complication to display no. Do you remember the dataForNow() method that we wrote a while ago as an extension on CollectionType? Well, we are going to use that now:

  func getCurrentTimelineEntryForComplication(complication: CLKComplication,
    withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {

      if let data = dataProvider.allDataForToday().dataForNow(){
        handler(timelineEntryForData(data))
      } else {
        handler(nil)
      }

  }
Note

Always implement the handlers that the class gives you. If they accept optional values and you don’t have any data to pass, just pass nil.

Now we have to implement the getTimelineEntriesForComplication(_:beforeDate:limit:beforeDate:) method of our complication delegate. This method gets called whenever watchOS needs timeline entries for data before a certain date, with a maximum of limit entries. So let’s say that you have 1,000 templates to return but the limit is 100. Do not return more than 100 in that case. In our example, I will go through all the data items that we have, filter them by their dates, find the ones coming before the given date (the beforeDate parameter), and create a timeline entry for all of those with the timelineEntryForData(_:) method that we wrote:

  func getTimelineEntriesForComplication(complication: CLKComplication,
    beforeDate date: NSDate, limit: Int,
    withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

      let entries = dataProvider.allDataForToday().filter{
        date.compare($0.date) == .OrderedDescending
      }.map{
        self.timelineEntryForData($0)
      }

      handler(entries)
  }

Similarly, we have to implement the getTimelineEntriesForComplication(_:afterDate:limit:withHandler:) method to return the timeline entries after a certain date (afterDate parameter):

  func getTimelineEntriesForComplication(complication: CLKComplication,
    afterDate date: NSDate, limit: Int,
    withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

      let entries = dataProvider.allDataForToday().filter{
        date.compare($0.date) == .OrderedAscending
      }.map{
        self.timelineEntryForData($0)
      }

      handler(entries)

  }

The getNextRequestedUpdateDateWithHandler(_:) method is the next method we need to implement. This method gets called to ask us when we would like to be asked for more data later. For our app we specify the next day, because we have already provided all the data for today:

  func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
    handler(NSDate.endOfToday());
  }

Last but not least, we have to implement the getPlaceholderTemplateForComplication(_:withHandler:) method that we talked about before. This is where we provide our placeholder template:

  func getPlaceholderTemplateForComplication(complication: CLKComplication,
    withHandler handler: (CLKComplicationTemplate?) -> Void) {
      if let data = dataProvider.allDataForToday().dataForNow(){
        handler(templateForData(data))
      } else {
        handler(nil)
      }
  }

Now when I run my app on my watch, because the time is 10:24 and the hour is 10, our complication will show 10 and fill the circle around it to show how much of the day has passed by 10:00 (see Figure 2-40).

Figure 2-40. Our complication on the bottom left is showing the hour

And if I engage time travel and move forward to 18:23, our complication updates itself as well, showing 18 as the hour (see Figure 2-41).

Figure 2-41. The user moves the time to the future and our complication updates itself as well

See Also

Recipe 2.7

2.9 Displaying Time Offsets in Complications

Problem

The data that you want to present has to be shown as an offset to a specific time. For instance, you want to show the remaining minutes until the next train that the user can take to get home.

Solution

Use the CLKRelativeDateTextProvider to provide your information inside a template. In this example, we are going to use CLKComplicationTemplateModularLargeStandardBody, which is a large and modular template.

Discussion

In this recipe, let’s create a watch app that shows the next available train that the user can take to get home. Trains can have different properties:

  • Date and time of departure
  • Train operator
  • Type of train (high speed, commuter train, etc.)
  • Service name (as shown on the time table)

In our example, I want the complication to look like Figure 2-42. The complication shows the next train (a Coastal service) and how many minutes away that train departs.

Figure 2-42. Complication shows that the next train leaves in 25 minutes

When you create your watchOS project, enable only the modular large complication in the target settings (see Figure 2-43).

Figure 2-43. Enable only the modular large complication for this example

Now create your data model. It will be similar to what we did in Recipe 2.8, but this time we want to provide train times. For the train type and the train company, create enumerations:

enum TrainType : String{
  case HighSpeed = "High Speed"
  case Commuter = "Commuter"
  case Coastal = "Coastal"
}

enum TrainCompany : String{
  case SJ = "SJ"
  case Southern = "Souther"
  case OldRail = "Old Rail"
}
Note

These enumerations are of type String, so you can display them on your UI easily without having to write a switch statement.

Then define a protocol to which your train object will conform. Protocol-oriented programming offers many possibilities (see Recipe 1.12), so let’s do that now:

protocol OnRailable{
  var type: TrainType {get}
  var company: TrainCompany {get}
  var service: String {get}
  var departureTime: NSDate {get}
}

struct Train : OnRailable{
  let type: TrainType
  let company: TrainCompany
  let service: String
  let departureTime: NSDate
}

As we did in Recipe 2.8, we are going to define a data provider. In this example, we create a few trains that depart at specific times with different types of services and from different operators:

struct DataProvider{

  func allTrainsForToday() -> [Train]{

    var all = [Train]()

    let now = NSDate()
    let cal = NSCalendar.currentCalendar()
    let units = NSCalendarUnit.Year.union(.Month).union(.Day)
    let comps = cal.components(units, fromDate: now)

    //first train
    comps.hour = 6
    comps.minute = 30
    comps.second = 0
    let date1 = cal.dateFromComponents(comps)!
    all.append(Train(type: .Commuter, company: .SJ,
      service: "3296", departureTime: date1))

    //second train
    comps.hour = 9
    comps.minute = 57
    let date2 = cal.dateFromComponents(comps)!
    all.append(Train(type: .HighSpeed, company: .Southern,
      service: "2307", departureTime: date2))

    //third train
    comps.hour = 12
    comps.minute = 22
    let date3 = cal.dateFromComponents(comps)!
    all.append(Train(type: .Coastal, company: .OldRail,
      service: "3206", departureTime: date3))

    //fourth train
    comps.hour = 15
    comps.minute = 45
    let date4 = cal.dateFromComponents(comps)!
    all.append(Train(type: .HighSpeed, company: .SJ,
      service: "3703", departureTime: date4))

    //fifth train
    comps.hour = 18
    comps.minute = 19
    let date5 = cal.dateFromComponents(comps)!
    all.append(Train(type: .Coastal, company: .Southern,
      service: "8307", departureTime: date5))

    //sixth train
    comps.hour = 22
    comps.minute = 11
    let date6 = cal.dateFromComponents(comps)!
    all.append(Train(type: .Commuter, company: .OldRail,
      service: "6802", departureTime: date6))

    return all

  }

}

Move now to the ComplicationController class of your watch extension. Here, you will provide watchOS with the data it needs to display your complication. The first task is to extend CollectionType so that you can find the next train in the array that the allTrainsForToday() function of DataProvider returns:

extension CollectionType where Generator.Element : OnRailable {

  func nextTrain() -> Generator.Element?{
    let now = NSDate()
    for d in self{
      if now.compare(d.departureTime) == .OrderedAscending{
        return d
      }
    }
    return nil
  }

}

And you need a data provider in your complication:

class ComplicationController: NSObject, CLKComplicationDataSource {

  let dataProvider = DataProvider()

  ...

For every train, you need to create a template that watchOS can display on the screen. All templates are of type CLKComplicationTemplate, but don’t initialize that class directly. Instead, create a template of type CLKComplicationTemplateModularLargeStandardBody that has a header, two lines of text with the second line being optional, and an optional image. The header will show a constant text (see Figure 2-42), so instantiate it of type CLKSimpleTextProvider. For the first line of text, you want to show how many minutes away the next train is, so that would require a text provider of type CLKRelativeDateTextProvider as we talked about it before.

The initializer for CLKRelativeDateTextProvider takes in a parameter of type CLKRelativeDateStyle that defines the way the given date has to be shown. In our example, we use CLKRelativeDateStyle.Offset:

  func templateForTrain(train: Train) -> CLKComplicationTemplate{
    let template = CLKComplicationTemplateModularLargeStandardBody()
    template.headerTextProvider = CLKSimpleTextProvider(text: "Next train")

    template.body1TextProvider =
      CLKRelativeDateTextProvider(date: train.departureTime,
        style: .Offset,
        units: NSCalendarUnit.Hour.union(.Minute))

    let secondLine = "(train.service) - (train.type)"

    template.body2TextProvider = CLKSimpleTextProvider(text: secondLine,
      shortText: train.type.rawValue)

    return template
  }
Note

The second line of text we are providing has a shortText alternative. If the watch UI has no space to show our secondLine text, it will show the shortText alternative.

We are going to need to provide timeline entries (date plus template) for every train as well, so let’s create a helper method for that:

  func timelineEntryForTrain(train: Train) -> CLKComplicationTimelineEntry{
    let template = templateForTrain(train)
    return CLKComplicationTimelineEntry(date: train.departureTime,
      complicationTemplate: template)
  }

When we are asked for the first and the last date of the data we provide, we read our data provider’s array of trains and return the first and the last train’s dates, respectively:

  func getTimelineStartDateForComplication(complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {
      handler(dataProvider.allTrainsForToday().first!.departureTime)
  }

  func getTimelineEndDateForComplication(complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {
    handler(dataProvider.allTrainsForToday().last!.departureTime)
  }

I want to allow the user to be able to time travel so that she can see the next train as she changes the time with the digital crown. I also believe our data is not sensitive, so I’ll allow viewing this data on the lock screen:

  func getSupportedTimeTravelDirectionsForComplication(
    complication: CLKComplication,
    withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {
      handler([.Forward, .Backward])
  }

  func getPrivacyBehaviorForComplication(complication: CLKComplication,
    withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {
    handler(.ShowOnLockScreen)
  }

Regarding time travel, when asked for trains after and before a certain time, your code should go through all the trains and filter out the times you don’t want displayed, as we did in Recipe 2.8:

                UU
  func getTimelineEntriesForComplication(complication: CLKComplication,
    beforeDate date: NSDate, limit: Int,
    withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

      let entries = dataProvider.allTrainsForToday().filter{
        date.compare($0.departureTime) == .OrderedDescending
      }.map{
        self.timelineEntryForTrain($0)
      }

      handler(entries)
  }

  func getTimelineEntriesForComplication(complication: CLKComplication,
    afterDate date: NSDate, limit: Int,
    withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

      let entries = dataProvider.allTrainsForToday().filter{
        date.compare($0.departureTime) == .OrderedAscending
      }.map{
        self.timelineEntryForTrain($0)
      }

      handler(entries)

  }

When the getCurrentTimelineEntryForComplication(_:withHandler:) method is called on our delegate, we get the next train’s timeline entry and return it:

  func getCurrentTimelineEntryForComplication(complication: CLKComplication,
    withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {

      if let train = dataProvider.allTrainsForToday().nextTrain(){
        handler(timelineEntryForTrain(train))
      } else {
        handler(nil)
      }

  }

Because we provide data until the end of today, we ask watchOS to ask us for new data tomorrow:

  func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
    handler(NSDate.endOfToday());
  }

Last but not least, we provide our placeholder template:

  func getPlaceholderTemplateForComplication(complication: CLKComplication,
    withHandler handler: (CLKComplicationTemplate?) -> Void) {
      if let data = dataProvider.allTrainsForToday().nextTrain(){
        handler(templateForTrain(data))
      } else {
        handler(nil)
      }
  }

We saw an example of our app showing the next train (see Figure 2-42), but our app can also participate in time travel (see Figure 2-44). The user can use the digital crown on the watch to move forward or backward and see the next available train at the new time.

Figure 2-44. Moving our complication backward in time

See Also

Recipe 2.7

2.10 Displaying Dates in Complications

Problem

You want to display NSDate instances on your complications.

Solution

Use an instance of the CLKDateTextProvider class, which is a subclass of CLKTextProvider, as your text provider.

Note

I am going to use CLKComplicationTemplateModularLargeColumns (a modular large template) for this recipe. So configure your watch target to provide only large-modular templates (see Figure 2-43).

Discussion

Let’s develop a modular large complication that provides us with the name and the date of the next three public holidays (see Figure 2-45). We are not formatting the date ourselves. We leave it to watchOS to decide how to display the date by using an instance of CLKDateTextProvider.

Figure 2-45. The next three public holidays, with their names and dates

Just as in Recipe 2.8 and Recipe 2.9, we are going to add a new class to our watch app called DataProvider. In there, we are going to program all the holidays this year. Let’s start off by defining what a holiday object looks like:

protocol Holidayable{
  var date: NSDate {get}
  var name: String {get}
}

struct Holiday : Holidayable{
  let date: NSDate
  let name: String
}

In our data provider class, we start off by defining some holiday names:

struct DataProvider{

  private let holidayNames = [
    "Father's Day",
    "Mother's Day",
    "Bank Holiday",
    "Nobel Day",
    "Man Day",
    "Woman Day",
    "Boyfriend Day",
    "Girlfriend Day",
    "Dog Day",
    "Cat Day",
    "Mouse Day",
    "Cow Day",
  ]

  private func randomDay() -> Int{
    return Int(arc4random_uniform(20) + 1)
  }

  ...

Then we move on to providing our instances of Holiday:

  func allHolidays() -> [Holiday]{

    var all = [Holiday]()

    let now = NSDate()
    let cal = NSCalendar.currentCalendar()
    let units = NSCalendarUnit.Year.union(.Month).union(.Day)
    let comps = cal.components(units, fromDate: now)

    var dates = [NSDate]()

    for month in 1...12{
      comps.day = randomDay()
      comps.month = month
      dates.append(cal.dateFromComponents(comps)!)
    }

    var i = 0
    for date in dates{
      all.append(Holiday(date: date, name: holidayNames[i++]))
    }

    return all

  }

It’s worth noting that the allHolidays() function we just wrote simply goes through all months inside this year, and sets the day of the month to a random day. So we will get 12 holidays, one in each month, at a random day inside that month.

Over to our ComplicationController. When we get asked later when we would like to provide more data or updated data to watchOS, we are going to ask for 10 minutes in the future. So if our data changes, watchOS will have a chance to ask us for updated information:

extension NSDate{
  func plus10Minutes() -> NSDate{
    return self.dateByAddingTimeInterval(10 * 60)
  }
}

Because the template we are going to provide allows a maximum of three items, I would like to have methods on Array to return the second and the third items inside the array, just like the prebuilt first property that the class offers:

extension Array{
  var second : Generator.Element?{
    return self.count >= 1 ? self[1] : nil
  }
  var third : Generator.Element?{
    return self.count >= 2 ? self[2] : nil
  }
}

DataProvider’s allHolidays() method returns 12 holidays. How about extending the built-in array type to always give us the next three holidays? It would have to read today’s date, go through the items in our array, compare the dates, and give us just the upcoming three holidays:

extension CollectionType where Generator.Element : Holidayable {

  //may contain less than 3 holidays
  func nextThreeHolidays() -> Array<Self.Generator.Element>{
    let now = NSDate()

    let orderedArray = Array(self.filter{
      now.compare($0.date) == .OrderedAscending
    })

    let result = Array(orderedArray[0..<min(orderedArray.count , 3)])

    return result
  }

}

Now we start defining our complication:

class ComplicationController: NSObject, CLKComplicationDataSource {

  let dataProvider = DataProvider()

  ...

We need a method that can take in a Holiday object and give us a template of type CLKComplicationTemplate for that. Our specific template for this recipe is of type CLKComplicationTemplateModularLargeColumns. This template is like a 3 × 3 table. It has three rows and three columns (see Figure 2-45). If we are at the end of the year and we have no more holidays, we return a template that is of type CLKComplicationTemplateModularLargeStandardBody and tell the user that there are no more upcoming holidays. Note that both templates have the words “ModularLarge” in their name. Because we have specified in our target setting that we support only modular large templates (see Figure 2-43), this example can return only templates that have those words in their name:

  func templateForHoliday(holiday: Holiday) -> CLKComplicationTemplate{

    let next3Holidays = dataProvider.allHolidays().nextThreeHolidays()

    let headerTitle = "Next 3 Holidays"

    guard next3Holidays.count > 0 else{
      let template = CLKComplicationTemplateModularLargeStandardBody()
      template.headerTextProvider = CLKSimpleTextProvider(text: headerTitle)
      template.body1TextProvider = CLKSimpleTextProvider(text: "Sorry!")
      return template
    }

    let dateUnits = NSCalendarUnit.Month.union(.Day)
    let template = CLKComplicationTemplateModularLargeColumns()

    //first holiday
    if let firstHoliday = next3Holidays.first{
      template.row1Column1TextProvider =
        CLKSimpleTextProvider(text: firstHoliday.name)
      template.row1Column2TextProvider =
        CLKDateTextProvider(date: firstHoliday.date, units: dateUnits)
    }

    //second holiday
    if let secondHoliday = next3Holidays.second{
      template.row2Column1TextProvider =
        CLKSimpleTextProvider(text: secondHoliday.name)
      template.row2Column2TextProvider =
        CLKDateTextProvider(date: secondHoliday.date, units: dateUnits)
    }

    //third holiday
    if let thirdHoliday = next3Holidays.third{
      template.row3Column1TextProvider =
        CLKSimpleTextProvider(text: thirdHoliday.name)
      template.row3Column2TextProvider =
        CLKDateTextProvider(date: thirdHoliday.date, units: dateUnits)
    }

    return template
  }

You need to provide a timeline entry (date plus template) for your holidays as well:

  func timelineEntryForHoliday(holiday: Holiday) ->
    CLKComplicationTimelineEntry{
    let template = templateForHoliday(holiday)
    return CLKComplicationTimelineEntry(date: holiday.date,
      complicationTemplate: template)
  }

Also provide the first and the last holidays:

  func getTimelineStartDateForComplication(complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {
      handler(dataProvider.allHolidays().first!.date)
  }

  func getTimelineEndDateForComplication(complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {
    handler(dataProvider.allHolidays().last!.date)
  }

Also support time travel and provide your content on the lock screen, because it is not private:

  func getSupportedTimeTravelDirectionsForComplication(
    complication: CLKComplication,
    withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {
      handler([.Forward, .Backward])
  }

  func getPrivacyBehaviorForComplication(complication: CLKComplication,
    withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {
    handler(.ShowOnLockScreen)
  }

Now let’s give watchOS information about previous and upcoming holidays:

  func getTimelineEntriesForComplication(complication: CLKComplication,
    beforeDate date: NSDate, limit: Int,
    withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

      let entries = dataProvider.allHolidays().filter{
        date.compare($0.date) == .OrderedDescending
      }.map{
        self.timelineEntryForHoliday($0)
      }

      handler(entries)
  }

  func getTimelineEntriesForComplication(complication: CLKComplication,
    afterDate date: NSDate, limit: Int,
    withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

      let entries = dataProvider.allHolidays().filter{
        date.compare($0.date) == .OrderedAscending
      }.map{
        self.timelineEntryForHoliday($0)
      }

      handler(entries)

  }

Last but not least, provide the upcoming three holidays when you are asked to provide them now:

  func getCurrentTimelineEntryForComplication(complication: CLKComplication,
    withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {

      if let first = dataProvider.allHolidays().nextThreeHolidays().first{
        handler(timelineEntryForHoliday(first))
      } else {
        handler(nil)
      }

  }

  func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
    handler(NSDate().plus10Minutes());
  }

  func getPlaceholderTemplateForComplication(complication: CLKComplication,
    withHandler handler: (CLKComplicationTemplate?) -> Void) {
      if let holiday = dataProvider.allHolidays().nextThreeHolidays().first{
        handler(templateForHoliday(holiday))
      } else {
        handler(nil)
      }
  }

See Also

Recipe 2.7 and Recipe 2.9

2.11 Displaying Times in Complications

Problem

You want to display a time on your watch UI and want it to look good regardless of available space on the watch.

Solution

Provide your time (in form of NSDate) to an instance of CLKTimeTextProvider and use it inside a template (see Figure 2-46). Our large and modular complication on the center of the screen is showing the next pause that we can take at work, which happens to be a coffee pause.

Figure 2-46. The time is displayed on the screen using an instance of CLKTimeTextProvider
Note

In this recipe, we are going to rely a lot on what we have learned in Recipe 2.8 and other complication recipes in this chapter. I suggest reading Recipe 2.8 at least to get an idea of how our data provider works. Otherwise, you will still be able to read this recipe; however, I will skip over some details that I’ve already explained in Recipe 2.8.

Discussion

This recipe uses a large-modular template, so make sure that your project is set up for that (see Figure 2-43). Here, I want to build an app that shows the different breaks or pauses that I can take at work, and when they occur: for instance, when the first pause is after I get to work, when lunch happens, when the next pause between lunch and dinner is, and if I want to have dinner as well, when that should happen.

So we have breaks at work and we need to define them. Create a Swift file in your watch extension and call it DataProvider. In there, define your break:

import Foundation

protocol Pausable{
  var name: String {get}
  var date: NSDate {get}
}

struct PauseAtWork : Pausable{
  let name: String
  let date: NSDate
}

Now in your DataProvider structure, create four pauses that we can take at work at different times and provide them as an array:

struct DataProvider{

  func allPausesToday() -> [PauseAtWork]{

    var all = [PauseAtWork]()

    let now = NSDate()
    let cal = NSCalendar.currentCalendar()
    let units = NSCalendarUnit.Year.union(.Month).union(.Day)
    let comps = cal.components(units, fromDate: now)
    comps.calendar = cal
    comps.minute = 30

    comps.hour = 11
    all.append(PauseAtWork(name: "Coffee", date: comps.date!))

    comps.minute = 30
    comps.hour = 14
    all.append(PauseAtWork(name: "Lunch", date: comps.date!))

    comps.minute = 0
    comps.hour = 16
    all.append(PauseAtWork(name: "Tea", date: comps.date!))

    comps.hour = 17
    all.append(PauseAtWork(name: "Dinner", date: comps.date!))

    return all

  }

}

Here we have just obtained the date and time of today and then gone from coffee break in the morning to dinner in the evening, adding each pause to the array. The method is called allPausesToday() and we are going to invoke it from our watch complication.

Before, we created a protocol called Pausable and now we have all our pauses in an array. When we are asked to provide a template for the next pause to show in the complication, we have to get the current time and find the pause whose time is after the current time. So let’s bundle that up by extending CollectionType like we have done in other recipes in this chapter:

extension CollectionType where Generator.Element : Pausable {

  func nextPause() -> Self.Generator.Element?{
    let now = NSDate()

    for pause in self{
      if now.compare(pause.date) == .OrderedAscending{
        return pause
      }
    }

    return nil
  }

}

In our complication now, we instantiate our data provider:

class ComplicationController: NSObject, CLKComplicationDataSource {

  let dataProvider = DataProvider()

  ...

For every pause that we want to display to the user (see Figure 2-46), we need to provide a template of type CLKComplicationTemplate to the runtime. We never instantiate that class directly. Instead, we return an instance of a subclass of that class. In this particular example, we display an instance of CLKComplicationTemplateModularLargeTallBody. However, if there are no more pauses to take at work (e.g., if time is 21:00 and we are no longer at work), we display a placeholder to the user to tell her there are no more pauses. The template for that is of type CLKComplicationTemplateModularLargeStandardBody. The difference between the two templates is visible if you read their names. We set the time on our template by setting the bodyTextProvider property of our CLKComplicationTemplateModularLargeTallBody instance:

  func templateForPause(pause: PauseAtWork) -> CLKComplicationTemplate{

    guard let nextPause = dataProvider.allPausesToday().nextPause() else{
      let template = CLKComplicationTemplateModularLargeStandardBody()
      template.headerTextProvider = CLKSimpleTextProvider(text: "Next Break")
      template.body1TextProvider = CLKSimpleTextProvider(text: "None")
      return template
    }

    let template = CLKComplicationTemplateModularLargeTallBody()
    template.headerTextProvider = CLKSimpleTextProvider(text: nextPause.name)
    template.bodyTextProvider = CLKTimeTextProvider(date: nextPause.date)

    return template
  }

We also have to provide some of the other delegate methods of CLKComplicationDataSource, such as the timeline entry (date plus template) for every pause that we can take at work. We also need to support time travel for this example. On top of that, our information is not sensitive, so when asked whether we want to display our complication on the lock screen, we happily say yes:

  func timelineEntryForPause(pause: PauseAtWork) ->
    CLKComplicationTimelineEntry{
    let template = templateForPause(pause)
    return CLKComplicationTimelineEntry(date: pause.date,
      complicationTemplate: template)
  }

  func getSupportedTimeTravelDirectionsForComplication(
    complication: CLKComplication,
    withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {
      handler([.Forward, .Backward])
  }

  func getPrivacyBehaviorForComplication(complication: CLKComplication,
    withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {
      handler(.ShowOnLockScreen)
  }

When asked the beginning and the end range of dates for our complications, we will return the dates for the first and the last pause that we want to take at work today. Remember, in this complication, we will return all the pauses that we can take at work today. When the time comes to display the pauses to take at work tomorrow, we will provide a whole set of new pauses:

  func getTimelineStartDateForComplication(complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {
      handler(dataProvider.allPausesToday().first!.date)
  }

  func getTimelineEndDateForComplication(complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {
    handler(dataProvider.allPausesToday().last!.date)
  }

When the runtime calls the getTimelineEntriesForComplication(_:beforeDate:limit:withHandler:) method, provide all the pauses that are available before the given date:

  func getTimelineEntriesForComplication(complication: CLKComplication,
    beforeDate date: NSDate, limit: Int,
    withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

      let entries = dataProvider.allPausesToday().filter{
        date.compare($0.date) == .OrderedDescending
      }.map{
        self.timelineEntryForPause($0)
      }

      handler(entries)
  }

Similarly, when the getTimelineEntriesForComplication(_:afterDate:limit:withHandler:) method is called, return all the available pauses after the given date:

  func getTimelineEntriesForComplication(complication: CLKComplication,
    afterDate date: NSDate, limit: Int,
    withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

      let entries = dataProvider.allPausesToday().filter{
        date.compare($0.date) == .OrderedAscending
      }.map{
        self.timelineEntryForPause($0)
      }

      handler(entries)

  }

In the getCurrentTimelineEntryForComplication(_:withHandler:) method, you will be asked to provide the template for the current data (the next pause) to show on screen. We already have a method on CollectionType called nextPause(), so let’s use that to provide a template to watchOS:

  func getCurrentTimelineEntryForComplication(complication: CLKComplication,
    withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {

      if let pause = dataProvider.allPausesToday().nextPause(){
        handler(timelineEntryForPause(pause))
      } else {
        handler(nil)
      }

  }

Because, in a typical watch app, our data would probably come from a backend, we would like the runtime to task us for up-to-date information as soon as possible, but not too soon. So let’s do that after 10 minutes:

  func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
    handler(NSDate().plus10Minutes());
  }

Last but not least, we also need to provide a placeholder template when the user is adding our complication to her watch face:

  func getPlaceholderTemplateForComplication(complication: CLKComplication,
    withHandler handler: (CLKComplicationTemplate?) -> Void) {
      if let pause = dataProvider.allPausesToday().nextPause(){
        handler(templateForPause(pause))
      } else {
        handler(nil)
      }
  }

See Also

Recipe 2.9 and Recipe 2.11

2.12 Displaying Time Intervals in Complications

Problem

You want to display a time interval (start date–end date) on your watchOS UI (see Figure 2-47). Our template shows today’s meetings on the screen. Right now, it’s brunch time, so the screen shows the description and location of where we are going to have brunch, along with the time interval of the brunch (start–end).

Figure 2-47. Meeting with start and end times

Solution

Use an instance of CLKTimeIntervalTextProvider as your text provider (see Figure 2-47).

Note

I will base this recipe on other recipes such as Recipe 2.10 and Recipe 2.11.

Discussion

Let’s say that we want to have an app that shows us all our meetings today. Every meeting has the following properties:

  • Start and end times (the time interval)
  • Name (e.g., “Brunch with Sarah”)
  • Location

Because text providers of type CLKSimpleTextProvider accept a short text in addition to the full text, we also have a short version of the location and the name. For instance, the location can be “Stockholm Central Train Station,” whereas the short version of this could be “Central Station” or even “Centralen” in Swedish, which means the center. So let’s define this meeting object:

protocol Timable{
  var name: String {get}
  var shortName: String {get}
  var location: String {get}
  var shortLocation: String {get}
  var startDate: NSDate {get}
  var endDate: NSDate {get}
  var previous: Timable? {get}
}

struct Meeting : Timable{
  let name: String
  let shortName: String
  let location: String
  let shortLocation: String
  let startDate: NSDate
  let endDate: NSDate
  let previous: Timable?
}

Create a Swift file in your project called DataProvider. Put all the meetings for today in there and return an array:

struct DataProvider{

  func allMeetingsToday() -> [Meeting]{

    var all = [Meeting]()

    let oneHour: NSTimeInterval = 1 * 60.0 * 60

    let now = NSDate()
    let cal = NSCalendar.currentCalendar()
    let units = NSCalendarUnit.Year.union(.Month).union(.Day)
    let comps = cal.components(units, fromDate: now)
    comps.calendar = cal
    comps.minute = 30

    comps.hour = 11
    let meeting1 = Meeting(name: "Brunch with Sarah", shortName: "Brunch",
      location: "Stockholm Central", shortLocation: "Central",
      startDate: comps.date!,
      endDate: comps.date!.dateByAddingTimeInterval(oneHour), previous: nil)
    all.append(meeting1)

    comps.minute = 30
    comps.hour = 14
    let meeting2 = Meeting(name: "Lunch with Gabriella", shortName: "Lunch",
      location: "At home", shortLocation: "Home",
      startDate: comps.date!,
      endDate: comps.date!.dateByAddingTimeInterval(oneHour),
      previous: meeting1)
    all.append(meeting2)

    comps.minute = 0
    comps.hour = 16
    let meeting3 = Meeting(name: "Snack with Leif", shortName: "Snack",
      location: "Flags Cafe", shortLocation: "Flags",
      startDate: comps.date!,
      endDate: comps.date!.dateByAddingTimeInterval(oneHour),
      previous: meeting2)
    all.append(meeting3)

    comps.hour = 17
    let meeting4 = Meeting(name: "Dinner with Family", shortName: "Dinner",
      location: "At home", shortLocation: "Home",
      startDate: comps.date!,
      endDate: comps.date!.dateByAddingTimeInterval(oneHour),
      previous: meeting3)
    all.append(meeting4)

    return all

  }

}

In your complication class, extend CollectionType so that it can return the upcoming meeting:

extension CollectionType where Generator.Element : Timable {

  func nextMeeting() -> Self.Generator.Element?{
    let now = NSDate()

    for meeting in self{
      if now.compare(meeting.startDate) == .OrderedAscending{
        return meeting
      }
    }

    return nil
  }

}
Note

I have extended CollectionType, but only if the items are Timable. I explained this technique in Recipe 1.12.

In your complication handler, create an instance of the data provider:

class ComplicationController: NSObject, CLKComplicationDataSource {

  let dataProvider = DataProvider()

  ...

Our template is of type CLKComplicationTemplateModularLargeStandardBody, which has a few important properties that we set as follows:

headerTextProvider
Shows the time range for the meeting.
body1TextProvider
Shows the name of the meeting.
body2TextProvider
Shows the location of the meeting.

To display the time range of the meeting, instantiate CLKTimeIntervalTextProvider:

  func templateForMeeting(meeting: Meeting) -> CLKComplicationTemplate{

    let template = CLKComplicationTemplateModularLargeStandardBody()

    guard let nextMeeting = dataProvider.allMeetingsToday().nextMeeting() else{
      template.headerTextProvider = CLKSimpleTextProvider(text: "Next Break")
      template.body1TextProvider = CLKSimpleTextProvider(text: "None")
      return template
    }

    template.headerTextProvider =
      CLKTimeIntervalTextProvider(startDate: nextMeeting.startDate,
        endDate: nextMeeting.endDate)

    template.body1TextProvider =
      CLKSimpleTextProvider(text: nextMeeting.name,
        shortText: nextMeeting.shortName)

    template.body2TextProvider =
      CLKSimpleTextProvider(text: nextMeeting.location,
        shortText: nextMeeting.shortLocation)

    return template
  }

Using this method, you can also create timeline entries (date plus template). In this example, I set every new event’s start date to the end date of the previous event (if it is available). That way, as soon as the current ongoing meeting ends, the next meeting shows up on the list:

Note

If the event has no previous events, its timeline entry date will be its start date, instead of the end date of the previous event.

  func timelineEntryForMeeting(meeting: Meeting) -> CLKComplicationTimelineEntry{
    let template = templateForMeeting(meeting)

    let date = meeting.previous?.endDate ?? meeting.startDate
    return CLKComplicationTimelineEntry(date: date,
      complicationTemplate: template)
  }

Let’s also participate in time travel and show our content on the lock screen as well:

  func getSupportedTimeTravelDirectionsForComplication(
    complication: CLKComplication,
    withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {
      handler([.Forward, .Backward])
  }

  func getPrivacyBehaviorForComplication(complication: CLKComplication,
    withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {
      handler(.ShowOnLockScreen)
  }

Then we have to provide the date range for which we have available meetings. The start of the range is the start date of the first meeting and the end date is the end date of the last meeting:

  func getTimelineStartDateForComplication(complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {
      handler(dataProvider.allMeetingsToday().first!.startDate)
  }

  func getTimelineEndDateForComplication(complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {
    handler(dataProvider.allMeetingsToday().last!.endDate)
  }

We’ll also be asked to provide all the available meetings before a certain date, so let’s do that:

  func getTimelineEntriesForComplication(complication: CLKComplication,
    beforeDate date: NSDate, limit: Int,
    withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

      let entries = dataProvider.allMeetingsToday().filter{
        date.compare($0.startDate) == .OrderedDescending
      }.map{
        self.timelineEntryForMeeting($0)
      }

      handler(entries)
  }

Similarly, we have to provide all our available meetings after a given date:

  func getTimelineEntriesForComplication(complication: CLKComplication,
    afterDate date: NSDate, limit: Int,
    withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

      let entries = dataProvider.allMeetingsToday().filter{
        date.compare($0.startDate) == .OrderedAscending
      }.map{
        self.timelineEntryForMeeting($0)
      }

      handler(entries)

  }

Last but not least, provide your placeholder template, the template for now, and the next time we would like watchOS to ask us for updated information:

  func getCurrentTimelineEntryForComplication(complication: CLKComplication,
    withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {

      if let meeting = dataProvider.allMeetingsToday().nextMeeting(){
        handler(timelineEntryForMeeting(meeting))
      } else {
        handler(nil)
      }

  }

  func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
    handler(NSDate().plus10Minutes());
  }

  func getPlaceholderTemplateForComplication(complication: CLKComplication,
    withHandler handler: (CLKComplicationTemplate?) -> Void) {
      if let pause = dataProvider.allMeetingsToday().nextMeeting(){
        handler(templateForMeeting(pause))
      } else {
        handler(nil)
      }
  }
Note

We coded the plus10Minutes() method on NSDate in Recipe 2.10.

2.13 Recording Audio in Your Watch App

Problem

You want to allow your users to record audio while inside your watch app, and you want to get access to the recorded audio.

Solution

Use the presentAudioRecorderControllerWithOutputURL(_:preset:options:completion:) method of your WKInterfaceController class to present a system dialog that can take care of audio recording. If you want to dismiss the dialog, use the dismissAudioRecordingController() method of your controller.

The options parameter of the presentAudioRecorderControllerWithOutputURL(_:preset:options:completion:) method accepts a dictionary that can contain the following keys:

WKAudioRecorderControllerOptionsActionTitleKey
This key, of type String, will be the title of our recorder.
WKAudioRecorderControllerOptionsAlwaysShowActionTitleKey
This key, of type NSNumber, contains a Bool value to dictates whether the title should always be shown on the recorder.
WKAudioRecorderControllerOptionsAutorecordKey
This key, of type NSNumber, contains a Bool value to indicate whether recording should begin automatically when the dialog is presented.
WKAudioRecorderControllerOptionsMaximumDurationKey
This key, of type NSNumber, contains an NSTimeInterval value to dictate the maximum duration of the audio content.

Discussion

For this recipe, we are going to create a watch app whose UI looks like that shown in Figure 2-48). It holds a label to show our current status (started recording, failed recording, etc.) and a button that, upon pressing, can show our recording dialog.

Figure 2-48. Label for status and button

Hook the label up to your code with the name statusLbl. Then hook your record button to your interface under a method named record(). Your interface code should look like this now:

class InterfaceController: WKInterfaceController {

  @IBOutlet var statusLbl: WKInterfaceLabel!

  ...

Define the URL where your recording will be saved:

  var url: NSURL{
    let fm = NSFileManager()
    let url = try! fm.URLForDirectory(NSSearchPathDirectory.MusicDirectory,
      inDomain: NSSearchPathDomainMask.UserDomainMask,
      appropriateForURL: nil, create: true)
      .URLByAppendingPathComponent("recording")
    return url
  }

Also, because the completion block of our recording screen might not get called on the main thread, create a variable that can set the text inside our status label on the main thread:

  var status = ""{
    willSet{
      dispatch_async(dispatch_get_main_queue()){
        self.statusLbl.setText(newValue)
      }
    }
  }

When your record button is pressed, construct your options for the recording:

    let oneMinute: NSTimeInterval = 1 * 60

    let yes = NSNumber(bool: true)
    let no = NSNumber(bool: false)

    let options = [
      WKAudioRecorderControllerOptionsActionTitleKey : "Audio Recorder",
      WKAudioRecorderControllerOptionsAlwaysShowActionTitleKey : yes,
      WKAudioRecorderControllerOptionsAutorecordKey : no,
      WKAudioRecorderControllerOptionsMaximumDurationKey : oneMinute
    ]

Last but not least, present your audio recorder to the user and then set the status accordingly:

    presentAudioRecorderControllerWithOutputURL(url,
      preset: WKAudioRecorderPreset.WideBandSpeech,
      options: options){
        success, error in

        defer{
          self.dismissAudioRecorderController()
        }

        guard success && error == nil else{
          self.status = "Failed to record"
          return
        }

        self.status = "Successfully recorded"

    }

See Also

Recipe 12.3

2.14 Playing Local and Remote Audio and Video in Your Watch App

Problem

You want to play audio or video files, whether they are saved locally or online.

Solution

Use the presentMediaPlayerControllerWithURL(_:options:completion:) instance method of your interface controller (WKInterfaceController). Close the media player with the dismissMediaPlayerController() method.

Discussion

The first parameter to this method is just the URL from which the media must be loaded. The options parameter is a dictionary that can have the following keys:

WKMediaPlayerControllerOptionsAutoplayKey
A boolean value (wrapped inside an NSNumber instance) that dictates whether the media should autoplay when it is opened. This is set to false by default.
WKMediaPlayerControllerOptionsStartTimeKey
The number of seconds (of type NSTimeInterval) into the media where you want to start it.
WKMediaPlayerControllerOptionsVideoGravityKey
A value of type WKVideoGravity (place its raw integer value in your dictionary) that dictates the scaling of the video. You can, for instance, specify WKVideoGravity.ResizeAspectFill.
WKMediaPlayerControllerOptionsLoopsKey
A boolean value (wrapped inside NSNumber) that specifies whether the media has to loop automatically. The default is false.

For this recipe, we are going to create a UI similar to that in Recipe 2.13 (see Figure 2-48). Our UI looks like Figure 2-49.

Figure 2-49. Label to show the current status, and a button to start the playback

Hook up the label to an outlet called statusLbl and the action of the button to a method called play(). Then create a variable in your code called status of type String, just as we did in Recipe 2.13. In the play method, first construct your URL:

    guard let url = NSURL(string: "http://localhost:8888/video.mp4") else{
      status = "Could not create url"
      return
    }
Note

I am running MAMP (free version) on my computer and I’m hosting a video called video.mp4. You can download lots of public domain files by just searching online.

Now construct your options dictionary. I want the media player to do the following:

  • Autoplay my video
  • Loop the video
  • Resize the video so that it fills the entire screen
  • Start at 4 seconds into the video:
    let gravity = WKVideoGravity.ResizeAspectFill.rawValue

    let options = [
      WKMediaPlayerControllerOptionsAutoplayKey : NSNumber(bool: true),
      WKMediaPlayerControllerOptionsStartTimeKey : 4.0 as NSTimeInterval,
      WKMediaPlayerControllerOptionsVideoGravityKey : gravity,
      WKMediaPlayerControllerOptionsLoopsKey : NSNumber(bool: true),
    ]

Now start the media player and handle any possible errors:

    presentMediaPlayerControllerWithURL(url, options: options) {
      didPlayToEnd, endTime, error in

      self.dismissMediaPlayerController()

      guard error == nil else{
        self.status = "Error occurred (error)"
        return
      }

      if didPlayToEnd{
        self.status = "Played to end of the file"
      } else {
        self.status = "Did not play to end of file. End time = (endTime)"
      }

    }

See Also

Recipe 12.3 and Recipe 2.13

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset