watchOS 2 Tutorial Part 4: Watch Connectivity

Mic Pringle
Note: This is a brand new tutorial released as part of the iOS 9 Feast. Enjoy!

Welcome back to our watchOS 2 tutorial series!

In the first part of this series, you learned about the basics of watchOS 2 development by creating your first interface controller.

In this second part of the series, you learned how to add tables to your app.

In this third part of the series, you learned how to use watchOS 2 animation.

In this fourth and final part of the series, you’ll learn how to use Watch Connectivity to fire off a request to your iPhone app to generate QR codes using Core Image. Thanks to Apple, this is much simpler than it sounds!

And with that, it’s time to crack on! ┗(°0°)┛

Note: This tutorial picks up where we left things off in the previous tutorial. You can either continue with the same project, or download it here if you don’t have it already.

Getting Started

Open Watch\Interface.storyboard and drag an Interface Controller from the Object Library onto the storyboard canvas. With the interface controller selected, open the Attributes Inspector and make the following changes:

  • Set Identifier to BoardingPass;
  • Set Insets to Custom;
  • Set the Top inset to 6.

As this interface is quite similar to the check-in interface you’re going to cheat a little bit, as building interfaces can get quite repetitive.

Expand CheckIn Scene in the Document Outline, select the group that contains the origin and destination labels, and select Edit\Copy:

Copy-From-Check-In

Then click anywhere in the new interface controller in the storyboard and select Edit\Paste. This only seems to work when you paste directly into the controller itself, rather than into the Document Outline, but I’m afraid I can’t tell you why.

Moving on, you’re new interface controller should now look like this:

Upper-Group

Next, drag an Image from the Object Library onto the new controller, making sure it’s positioned as a sibling of the group you just pasted, rather than a child:

Image-Sibling

This image is dual-purpose; initially it’ll display an animated image sequence to indicate to the user that something’s happening, and then when the watch receives the boarding pass from the phone, the image will display it.

Download this zip file, unzip the file, and drag the folder into your Watch\Assets.xcassets.

Make sure you drag the folder and not it’s contents. This should create a new group in the asset catalog called Activity, containing several image sets:

Activity-Image-Group

This is the image sequence that’ll act as the indeterminate progress indicator when you request the boarding pass from the paired phone.

Re-open Watch\Interface.storyboard and select the image. Using your ol’ friend the Attributes Inspector, make the following changes:

  • Set Image to Activity. The autocomplete might suggest something like “Activity1” so make sure you enter just Activity;
  • Set Animate to Yes;
  • Set Duration to 1;
  • Check Animate on Load;
  • Set the Horizontal alignment to Center;
  • Set the Vertical alignment to Center;
  • Set Width to Fixed, with a value of 66;
  • Set Height to Fixed, with a value of 66.

After making the changes, your Attributes Inspector should look like this:

Image-Attributes

The interface controller should look like the following:

Interface-Completed

Don’t worry about the image preview being a big blurry question mark; Interface Builder doesn’t preview animated images at this time, and since there isn’t technically an image named Activity – remember, all the images are suffixed with a number – Interface Builder is quite rightly stating it can’t load the image. This is all resolved at runtime though, trust me.

That’s it for the boarding pass interface. Now to create a WKInterfaceController subclass that does the heavy-lifting.

Creating the Controller

Right-click on the Watch Extension group in the Project Navigator and choose New File…. In the dialog that appears select watchOS\Source\WatchKit Class and click Next. Name the new class BoardingPassInterfaceController, and make sure it’s subclassing WKInterfaceController and that Language is set to Swift:

File-Options

Click Next, and then Create.

When the new file opens in the code editor, delete the three empty method stubs so you’re left with just the import statements and the class definition.

Then, add the following outlets at the top of the class:

@IBOutlet var originLabel: WKInterfaceLabel!
@IBOutlet var destinationLabel: WKInterfaceLabel!
@IBOutlet var boardingPassImage: WKInterfaceImage!

Here you’re simply adding outlets for the image and the two labels you just created. You’ll connect these up is just a moment.

Now, add the following just below the outlets:

var flight: Flight? {
  didSet {
    if let flight = flight {
      originLabel.setText(flight.origin)
      destinationLabel.setText(flight.destination)
    }
  }
}

It’s our ol’ friend flight and it’s property observer! I bet you were wondering if it was going to make an appearance this time around. You know what’s going on by now, but just to recap, you’ve added an optional property of type Flight, which includes a property observer. When the observer is fired, you try to unwrap flight, and if that succeeds you use flight to configure the two labels.

Now you just need to set flight when the controller is first presented. Add the following to BoardingPassInterfaceController:

override func awakeWithContext(context: AnyObject?) {
  super.awakeWithContext(context)
  if let flight = context as? Flight { self.flight = flight }
}

Another old friend; I guess you could call this a reunion of sorts! But just in case you’ve forgotten what this does, you try to unwrap and cast context to an instance of Flight, and if that succeeds you use it to set self.flight, which in-turn triggers the property observer and configures the interface.

That’s it for the boilerplate code in this exercise, I promise. :]

Now, open up Watch\Interface.storyboard and select the boarding pass interface controller. In the Identity Inspector, change Custom Class\Class to BoardingPassInterfaceController:

Custom-Class

Then, right-click on BoardingPass in the Document Outline to invoke the outlets and actions popup. Connect boardingPassImage to the image:

Image-Outlet

Finally, connect destinationLabel to the label containing SFO, and connect originLabel to the label containing MAN.

With that done, you now need to update ScheduleInterfaceController so it presents the boarding pass interface controller once a user has checked-in.

Presenting the Boarding Pass Interface

Open ScheduleInterfaceController.swift and find table(_:didSelectRowAtIndex:). Replace this statement:

let controllers = ["Flight", "CheckIn"]

With this one:

let controllers = flight.checkedIn ? ["Flight", "BoardingPass"] : ["Flight", "CheckIn"]

Here you’re simply checking whether or not the user has checked-in for the selected flight, and if so you present the flight details and boarding pass interface controllers. If they haven’t, you present the flight details and check-in interface controllers instead.

Build and run. Tap the first flight, swipe left, and tap Check In. Tap the same flight again, swipe left, and you’ll now see the boarding pass interface controller instead, displaying the indeterminate progress indicator:

Progress-Indicator

It’s now time to dig into the new Watch Connectivity framework and request the actual boarding pass.

Requesting the Boarding Pass

Open BoardingPassInterfaceController.swift and import the Watch Connectivity framework:

import WatchConnectivity

Next, add the following property just below where you’ve declared flight:

var session: WCSession? {
  didSet {
    if let session = session {
      session.delegate = self
      session.activateSession()
    }
  }
}

Here you’ve added a new optional property of the type WCSession. All communication between the two devices, your watch and phone, is handled by WCSession; you don’t instantiate an instance of this class yourself, rather you use a singleton provided by the framework. You’ve also added a property observer that, when triggered, attempts to unwrap session. If that succeeds then it sets the session’s delegate before activating it.

Even though you won’t be implementing any of the delegate methods in this class, you’re still required to set the delegate prior to activating the session, otherwise things get a bit unpredictable.

Xcode will now likely be complaining that BoardingPassInterfaceController doesn’t conform to WCSessionDelegate, so add the following empty extension at the very bottom of BoardingPassInterfaceController.swift, outside of the class:

extension BoardingPassInterfaceController: WCSessionDelegate {

}

Next, add the following helper method to BoardingPassInterfaceController:

private func showBoardingPass() {
  boardingPassImage.stopAnimating()
  boardingPassImage.setWidth(120)
  boardingPassImage.setHeight(120)
  boardingPassImage.setImage(flight?.boardingPass)
}

This will be called from two places – from within the property observer of flight if the flight already has a boarding pass, and from within the reply handler of the message you fire off to the iPhone. The implementation is pretty straightforward – you stop the image animating, increase the size of the image, and then set the image being displayed to the boarding pass.

You’ll update the property observer first. Add the following to the bottom of if statement in the property observer for flight:

if let _ = flight.boardingPass {
  showBoardingPass()
}

Here you call showBoardingPass() only if flight already has a boarding pass.

The final piece of the puzzle is to actually send the request off to the iPhone. Add the following just below awakeWithContext(_:):

override func didAppear() {
  super.didAppear()
  // 1
  if let flight = flight where flight.boardingPass == nil && WCSession.isSupported() {
    // 2
    session = WCSession.defaultSession()
    // 3
    session!.sendMessage(["reference": flight.reference], replyHandler: { (response) -> Void in
      // 4
      if let boardingPassData = response["boardingPassData"] as? NSData, boardingPass = UIImage(data: boardingPassData) {
        // 5
        flight.boardingPass = boardingPass
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
          self.showBoardingPass()
        })
      }
    }, errorHandler: { (error) -> Void in
      // 6
      print(error)
    })
  }
}

Here’s the play-by-play of what’s happening in the code above:

  1. If you have a valid flight that has no boarding pass, and Watch Connectivity is supported, then you move onto sending the message. You should always check to see if Watch Connectivity is supported before attempting any communication with the paired phone.
  2. You set session to the default session singleton. This in-turn triggers the property observer, setting the session’s delegate before activating it.
  3. You fire off the message to the companion iPhone app. You include a dictionary containing the flight reference that will be forwarded to the iPhone app, and provide both reply and error handlers.
  4. The reply handler receives a dictionary, and is called by the iPhone app. You first try to extract the image data of the boarding pass from the dictionary, before attempting to create an instance of UIImage with it.
  5. If that succeeds, you set the image as the flight’s boarding pass, and then jump over to the main queue where you call showBoardingPass() to show it to the user. The reply and error handlers are called on a background queue, so if you need to update the interface, as you are here, then always make sure to jump to the main queue before doing so.
  6. If the message sending fails then you simply print the error to the console.

That’s the watch app side of the conversation catered for. Now you need to update the iPhone app.

Responding to Requests

Open AppDelegate.swift, which can be found in the AirAber group in the Project Navigator.

First, import the Watch Connectivity framework:

import WatchConnectivity

Then, add the following property just below window:

var session: WCSession? {
  didSet {
    if let session = session {
      session.delegate = self
      session.activateSession()
    }
  }
}

This behaves exactly the same as its namesake in BoardingPassInterfaceController. It’s simply an optional property of the type WCSession, which includes a property observer that, when triggered, attempts to unwrap session. If that succeeds then it sets the session’s delegate, before activating it.

Next, add the following extension outside of the class in AppDelegate.swift:

extension AppDelegate: WCSessionDelegate {

  func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
    if let reference = message["reference"] as? String, boardingPass = QRCode(reference) {
      replyHandler(["boardingPassData": boardingPass.PNGData])
    }
  }

}

Here you implement the WCSessionDelegate method responsible for receiving realtime messages. In it, you extract the flight reference from the dictionary you passed above, and then use that to generate a QR code with the amazing QRCode library from Alexander Schuch. If that’s successful then you call the reply handler, passing the image data back to the watch app.

Finally, you need to set session. Add the following to application(_:didFinishLaunchingWithOptions:), just above the return statement:

if WCSession.isSupported() {
  session = WCSession.defaultSession()
}

Here you make sure Watch Connectivity is supported, and if it is you set session to the default session singleton provided by the framework.

And with that you should now be able to have a two-way conversation with the iPhone app.

Build and run. Follow the steps above to check-in and then view the boarding pass. This time around the boarding pass should appear after a short while:

Boarding-Pass

Note: If the boarding pass doesn’t appear at the first time of asking, try a second time. This seems to happen intermittently, and appears to be an issue with the latest beta of Xcode 7, as it worked every time without fail in the earlier betas.

Congratulations! You’ve now finished adding support for requesting boarding passes from the iPhone app using Watch Connectivity; nice work.

Where to Go From Here?

Here is the finished example project from this tutorial series.

In this tutorial you’ve learned how to send realtime messages between the watch app and companion iPhone app, and how to use them to transfer image data between the two devices.

But Watch Connectivity doesn’t stop there, oh no! Remember, you can also use the framework to update the application context, as well as send and receive background messages and files, which are delivered even when the receiving app isn’t running.

If you enjoyed this series and would like to learn more about developing for watchOS 2, check out our book watchOS 2 by Tutorials that teaches you everything you need to know to make great watchOS 2 apps.

If you have any questions or comments on this tutorial, please join the forum discussion below! :]

Mic Pringle

Mic Pringle is a developer first and foremost, but regularly turns his hand to book and tutorial editing, video tutorial making, and hosting the official raywenderlich.com podcast. Mic is 1/6th of Razeware, the team behind raywenderlich.com. An iOS developer by trade, Mic has recently found himself dabbling in Ruby, PHP, and JavaScript.

When not knee-deep in code, Mic enjoys spending time with his wife Lucy and their daughter Evie, watching his beloved Fulham F.C., and weather permitting, tearing up the English countryside on his Harley Davidson.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

Swift Team

... 15 total!

iOS Team

... 44 total!

Android Team

... 15 total!

macOS Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 15 total!

Resident Authors Team

... 17 total!

Podcast Team

... 8 total!

Recruitment Team

... 9 total!