iOS 9 Swift Programming Cookbook (2015)
Chapter 2. Apple Watch
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 implementsession(_: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 4 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 calledPredefinedMessages 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 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 PredefinedMessageenum 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 thesession(_: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, your 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
See Also