NEW! Master Swift design patterns with my latest book! >>

< Previous: How to measure touch strength using 3D Touch   Next: Designing a simple watchOS app to receive data >

Communicating between iOS and watchOS: WCSession

It's time for something new, and something I've held back from covering in Hacking with Swift because only a small proportion of people have an Apple Watch. So, I'm covering it here only briefly, and only at the very end of the project so that if you don't have an Apple Watch you can just skip on past.

Still here? OK: we're going to upgrade our project so that when your finger moves over the correct card your Apple Watch will gently tap your wrist. The haptic vibration of Apple Watches is so marvelously subtle that no one will have any idea what's happening – the effect is very impressive!

I have good news and bad news. First the good news: for our purposes, communicating between Apple Watch and iOS could not be any easier – it take us maybe five minutes in total to complete the code. Now the bad news: even when the settings are adjusted, your Watch will go to sleep after 70 seconds of inactivity, so it's down you to make sure the app stays awake.

That bad news will make more sense once you're using the finished product, so without further ado let's crack on with development. In ViewController.swift add this new import:

import WatchConnectivity

As you might imagine, the WatchConnectivity framework is responsible for connectivity between iOS apps and watchOS apps, and we'll be using it to send messages between our phone and a Watch. The messages are dictionaries of any data you want, so you can send strings, numbers, arrays and more – it's up to you.

In order to work with a session, we need to check whether it's supported on our current phone, then activate it. Put this code into the viewDidLoad() method of ViewController.swift:

if (WCSession.isSupported()) {
    let session = WCSession.default
    session.delegate = self
    session.activate()
}

You'll get an error because the ViewController class doesn't conform to the WCSessionDelegate protocol, but that's easily fixed:

class ViewController: UIViewController, WCSessionDelegate {

You need to add a few methods to ViewController in order to satisfy this new protocol, but all of them can be empty because we don’t actually care about them. Add these three at the end of the class:

func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {

}

func sessionDidBecomeInactive(_ session: WCSession) {

}

func sessionDidDeactivate(_ session: WCSession) {

}

(Note: if you were wondering, you can’t call activate() on a session without a delegate. We don’t actually use any of the delegate methods, but we still need to assign a delegate!)

Sending a message from a phone to a watch is trivial, like I said, but there is one small piece of complexity: if we want the watch to buzz every time it receives a message (spoiler: that's exactly what we want), we need a way to rate limit those messages. That is, we don't want to send 100 messages a second when the user is touching the right card, because it would make your watch go nuts.

To solve this problem, we're going to add a new property that tracks when the last watch message was sent. This way, we can avoid sending a message to the watch if there was one sent very recently – i.e., less than about half a second ago.

Add this property to the class:

var lastMessage: CFAbsoluteTime = 0

If you were wondering, CFAbsoluteTime is actually just a Double behind the scenes. We can get the current time using a function called CFAbsoluteTimeGetCurrent(), which returns the number of seconds that have passed since midnight on January 1st 2001. Yes, that's a rather random date, but it doesn't matter: all we care about is the time since our previous call.

Sending a message from the app to the watch is done in two parts. First, we need to check whether the watch is reachable, which in practice means "is our Apple Watch app running and in the foreground?" Second, we need to use the sendMessage() method of WCSession to send a dictionary of data. It doesn't matter what data we send, because in our app any data will be interpreted as "please buzz."

Keeping in mind the need to rate limit these calls, here's a new method for the ViewController class:

func sendWatchMessage() {
    let currentTime = CFAbsoluteTimeGetCurrent()

    // if less than half a second has passed, bail out
    if lastMessage + 0.5 > currentTime {
        return
    }

    // send a message to the watch if it's reachable
    if (WCSession.default.isReachable) {
        // this is a meaningless message, but it's enough for our purposes
        let message = ["Message": "Hello"]
        WCSession.default.sendMessage(message, replyHandler: nil)
    }

    // update our rate limiting property
    lastMessage = CFAbsoluteTimeGetCurrent()
}

With that new method in place we can call it inside touchesMoved() by adding this code at the end of the contains() condition:

if card.isCorrect {
    sendWatchMessage()
}

Just in case you're not sure where I mean, here's how the complete touchesMoved() method should look:

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesMoved(touches, with: event)

    guard let touch = touches.first else { return }
    let location = touch.location(in: cardContainer)

    for card in allCards {
        if card.view.frame.contains(location) {
            if view.traitCollection.forceTouchCapability == .available {
                if touch.force == touch.maximumPossibleForce {
                    card.front.image = UIImage(named: "cardStar")
                    card.isCorrect = true
                }
            }

            // here's the new code!
            if card.isCorrect {
                sendWatchMessage()
            }
        }
    }
}

And that's it. Yes, that's all the code it takes to send data from our iOS app to an Apple Watch. Of course, the app won't do anything yet because sending data isn't enough: we need to write code to receive it and do something interesting.

Help support Hacking with Swift

This site is funded by Hacking with Swift supporters who buy my e-books. If you can, please support my work – it comes packed with bonus material!

< Previous: How to measure touch strength using 3D Touch   Next: Designing a simple watchOS app to receive data >
MASTER SWIFT NOW
Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Practical iOS 11 Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let me know!

Average rating: 4.0/5

Click here to visit the Hacking with Swift store >>