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°)┛
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:
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:
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:
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:
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:
The interface controller should look like the following:
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:
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
:
Then, right-click on BoardingPass in the Document Outline to invoke the outlets and actions popup. Connect boardingPassImage
to the image:
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:
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:
- 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.
- You set
session
to the default session singleton. This in-turn triggers the property observer, setting the session’s delegate before activating it. - 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.
- 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. - 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. - 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:
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! :]
The post watchOS 2 Tutorial Part 4: Watch Connectivity appeared first on Ray Wenderlich.