watchOS

watchOS 2 Tutorial: Using sendMessage for Instantaneous Data Transfer (Watch Connectivity #1)

I wrote about the many ways that you can communicate between the Apple Watch and your iPhone app a couple of weeks ago for watchOS 2. Over the next month, I’ll be releasing a series of deep dives on the different ways of communicating using the Watch Connectivity framework. These will be various tutorial projects centered around the best use case for each of these communication options. The first of these will focus on instantaneous messaging using sendMessage:replyHandler:errorHandler: to transfer a dictionary of information between the devices. For a complete look at the various communication options, refer back to this post.

You can find the finished sample code here in both Objective-C and Swift under WatchOS2CounterObjC and WatchOS2CounterSwift. I’ll be upgrading an old tutorial that I wrote earlier – creating a counter app on the watch that allows the user to save count values to a table view in the iPhone (parent) app. This is what it’ll look like in the end:

Counter App watchOS 2

Let’s get started 🙂

Counter Watch App

First, download the starter code – Swift or Objective-C. This project fills out the iOS table view portion of the app so we don’t have to go through it. It’ll handle things like setting up the table view, updating the table view cells and other tedious things like that. Open the project and you’ll see we have three folders in our project hierarchy – WatchOS2Counter, WatchOS2Counter WatchKit

Open the project and you’ll see we have three folders in our project hierarchy – WatchOS2Counter, WatchOS2Counter WatchKit Extension and WatchOS2Counter WatchKit App. We’re going to focus on this last one first.

Here, we’re going to create the user interface. You’ll want to model yours after the one here.

Interface for Counter

To create this, drag in a label, change the text to “0” and in the attributes inspector (the bookmark icon in the sidebar), set its width to “relative to container.” You can also increase the size of the font since this counter label will be the main focus of the watch app. I changed it to size 21.

Next you’ll want to drag in a group and place it underneath the label. This allows us to place the Hit and Clear buttons side-by-side. Drag in two buttons and place them next to each other. You’ll see now that one of the buttons will be offscreen since the first one you dragged in takes up the entire width of the screen. You can change this by going to the attributes sidebar and modifying the width to be “.5” which stands for 50% of the screen. Do this for the other button too and both buttons should fit on the same row now. Change the text of the buttons to Hit and Clear. Drag in one last button underneath the group and call it Save.

WatchKit Extension

Open up the assistant editor (split view editor) with the Watch Storyboard and  InterfaceController.m/InterfaceController.swift in the WatchKit extension.  Add the following two properties:

//Swift
@IBOutlet var counterLabel: WKInterfaceLabel!
var counter = 0
//Objective-C
@property (unsafe_unretained, nonatomic) IBOutlet WKInterfaceLabel *counterLabel;
@property (nonatomic, assign) int counter;

Now, drag in an outlet from the counter label and attach it to the counterLabel property. Next, you’ll want to add the following functions:

//Swift
@IBAction func incrementCounter() {
    counter++
    setCounterLabelText()
}
    
@IBAction func clearCounter() {
    counter = 0
    setCounterLabelText()
}
    
@IBAction func saveCounter() {
}
    
func setCounterLabelText() {
    counterLabel.setText(String(counter))
}
//Objective-C
- (IBAction)incrementCounter {
    self.counter++;
    [self setCounterLabelText];
}

- (IBAction)clearCounter {
    self.counter = 0;
    [self setCounterLabelText];
}

- (IBAction)saveCounter {
}

#pragma mark - Helper methods

- (void)setCounterLabelText {
    [self.counterLabel setText:[NSString stringWithFormat:@"%d", self.counter]];
}

Connect the Watch app buttons with the correct functions.
Hit – incrementCounter
Clear – clearCounter
Save – saveCounter

I’ve added in a helper method at the bottom to help format the counter label. You’ll also notice I left saveCounter blank. Don’t worry, we’ll get back to that later. First, I want to talk about the whole reason for this tutorial – the new Watch Connectivity framework.

Watch Connectivity

Using Watch Connectivity is fairly straightforward. The main premise is that you can now send and receive data from either device through WCSession. Once a session is established on both devices, you can pass messages back and forth as much as your app needs it. To get started, we’ll first have to check that WCSession is supported in our apps.

First, import Watch Connectivity and add WCSessionDelegate to the interface header in InterfaceController.m/InterfaceController.swift like so:

//Swift
import WatchConnectivity
class InterfaceController: WKInterfaceController, WCSessionDelegate {
//Objective-C
#import <WatchConnectivity/WatchConnectivity.h>;
@interface InterfaceController() 

Replace willActivate with the following:

//Swift
var counter = 0
var session : WCSession!

override func willActivate() {
    super.willActivate()
        
    if (WCSession.isSupported()) {
        session = WCSession.defaultSession()
        session.delegate = self
        session.activateSession()
    }
}
//Objective-C
- (void)willActivate {
    [super willActivate];
    
    if ([WCSession isSupported]) {
        WCSession *session = [WCSession defaultSession];
        session.delegate = self;
        [session activateSession];
    }
    
    self.counter = 0;
}

This is the basic way to set up a WCSession. For every class that you want to send and receive data in, you’ll need to define the WCSession like this each time. Now that we have our session set up, we can go back to our saveCounter function.

Now it should look like this:

//Swift
@IBAction func saveCounter() {
    let applicationData = ["counterValue":String(counter)]
        
    session.sendMessage(applicationData, replyHandler: {([String : AnyObject]) -> Void in
        // handle reply from iPhone app here
    }, errorHandler: {(error ) -> Void in
        // catch any errors here
    }) 
}
//Objective-C
- (IBAction)saveCounter {
    NSString *counterString = [NSString stringWithFormat:@"%d", self.counter];
    NSDictionary *applicationData = [[NSDictionary alloc] initWithObjects:@[counterString] forKeys:@[@"counterValue"]];
    
    [[WCSession defaultSession] sendMessage:applicationData
                               replyHandler:^(NSDictionary *reply) {
                                   //handle reply from iPhone app here
                               }
                               errorHandler:^(NSError *error) {
                                   //catch any errors here
                               }
     ];
}

When the user hits the save button, we’ll want to transfer the saved counter value to our table view in the main app. First we’ll do some necessary formatting and save the counterValue into a dictionary named applicationData. From here, we’ll use the sendMessage:replyHander:errorHandler: API to pass this dictionary to our parent app.

We’re finished with the WatchKit extension. Now let’s head to the iOS app to receive the dictionary on the other side.

Parent iOS app – ViewController.m/ViewController.swift

Remember, we have to add WCSession to every class that needs to send and receive data, so we’ll go ahead and set it up here again. Import Watch Connectivity, add WCSessionDelegate to the ViewController header and insert the basic setup code in viewDidLoad. It should look something like this:

//Swift
import UIKit
import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate, UITableViewDataSource {

    @IBOutlet weak var mainTableView: UITableView!
    var counterData = [String]()
    var session: WCSession!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if (WCSession.isSupported()) {
            session = WCSession.defaultSession()
            session.delegate = self;
            session.activateSession()
        }
        
        self.mainTableView.reloadData()
    }

//Objective-C
#import "ViewController.h"
#import <WatchConnectivity/WatchConnectivity.h>

@interface ViewController () <WCSessionDelegate, UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *mainTableView;
@property (strong, nonatomic) NSMutableArray *counterData;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    if ([WCSession isSupported]) {
        WCSession *session = [WCSession defaultSession];
        session.delegate = self;
        [session activateSession];
    }
    
    if (!self.counterData) {
        self.counterData = [[NSMutableArray alloc] init];
    }
    
    [self.mainTableView reloadData];
}

There’s one last thing to add in this file and that’s the receiving end of message we sent over from the watch extension. Add this function to the bottom of your file:

//Swift
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
    let counterValue = message["counterValue"] as? String
    
    //Use this to update the UI instantaneously (otherwise, takes a little while)
    dispatch_async(dispatch_get_main_queue()) {
        self.counterData.append(counterValue!)
        self.mainTableView.reloadData()
    }
}

//Objective-C
- (void)session:(nonnull WCSession *)session didReceiveMessage:(nonnull NSDictionary *)message replyHandler:(nonnull void (^)(NSDictionary * __nonnull))replyHandler {
    NSString *counterValue = [message objectForKey:@"counterValue"];
    
    if (!self.counterData) {
        self.counterData = [[NSMutableArray alloc] init];
    }
    
    //Use this to update the UI instantaneously (otherwise, takes a little while)
    dispatch_async(dispatch_get_main_queue(), ^{
        
        [self.counterData addObject:counterValue];
        [self.mainTableView reloadData];
    });
}

Here, we’re pulling the counterValue out of the message dictionary and adding it to the mainTableView. You’ll want to do this using dispatch_async, which allows the UI in the parent app to update instantaneously. For more complex apps, if you have multiple transfers going back and forth between your watch and parent iOS app, you can use identifiers to determine what you want to do in the data receiver functions such as this.

That’s it! Not too bad right? I like this a lot better than using openParentApplication from watchOS 1. It’s so much more flexible being able to send and receive data where ever it’s needed. Let me know your thoughts and questions below.

37 thoughts on “watchOS 2 Tutorial: Using sendMessage for Instantaneous Data Transfer (Watch Connectivity #1)”

      1. Hi Kristina when am trying to reload table view data in ” sendMessageData:replyHandler:errorHandler: ” this method like this
        DispatchQueue.main.async {

        self.tableView.reload()
        }

        i am getting called by table view data source.i.e num of rows in a section method and i am getting data also in array. but it is not calling the Cell for Row at index path .please let me know .how can i update my Tableview rows.with the data comming form Watch in this sendMessageData:replyHandler:errorHandler: method.

  1. In the Watch Connectivity WWDC tutorial, speaker mentions need to activate session before viewDidLoad, especially since iPhone app may be launched in background (and viewDidLoad will not be called?). Thoughts?

    1. Definitely can be an issue, especially if your apps are running in the background and views aren’t loaded. Another suggestion may be to activate the session in an initializer, or where ever it’s actually being used.

  2. Kristina these are amazing demos !! Very usefull !! I haveing a few issues though it doenst seem in my GlanceController the WCSession never becomes reachable so i cant make calls to the iphone, can you think of any reason why Glances wouldnt work but the standard interface does? Thanks!

    1. I recently chatted with someone about this. Apparently, both the watch app and the watch glance share a single WCSession delegate so any data that you’re trying to pass to your glance is probably being intercepted by your watch app instead. Essentially, only your watch app or your glance can receive messages, but unfortunately, not both. There might be a workaround, but I haven’t played around enough with glances to give you anything concrete. Would love to hear if anyone figures it out.

  3. Hi Kristina, Again, agree with everyone, amazing tutorial! really helpful for me too as my iOS app is obj-c and my watch app, swift!.
    Im having a problem using the didrecievemessage and getting it to run when my app is close. It runs if the app has been opened recently and just left in background but it doesn’t launch the app from nothing. I think it maybe due to a lack of a correct app delegate call in the obj-c iOS app on the iPhone, any suggestions. Thanks again

  4. Hi, unfortunately this will only work in simulator over here. I’m not able to send messages from my iPhone to my Apple Watch as a real device.
    It seems to be a bug as I’m getting my paired device [Apple Watch] ‘is not reachable’.
    Could someone please proove me wrong and tell me he/she is able to get it work on real devices. Would appreciated it so much!

  5. In my case, I´m not able to wake up the phone with the sendMessage Method. I´m setting the WCSessionDelegate in the iOS AppDelegate, and ofc handling the didReceiveMessage in there as well.

    Although I want to check it using real devices, I´d like to know if this happens because I´m using the simulator. Does it matter to activate the session in the app delegate or any VC to make it able to be woken up? Is any additional setup in the appDelegate required?

    Thanks!

    1. Could you clarify what you mean by “wake up the phone?” Using sendMessage from the watch to the phone won’t launch your phone app in the foreground, it will only launch in the background if it isn’t currently active in order to complete the message transfer. It shouldn’t matter where you’re activating the session (in fact, it’s better to do it in the app delegate) and there shouldn’t be any additional setup needed.

      1. Yeah, what I meant is that I can’t see in the debugger any print that I’m doing in the app delegate until I manually start the app in the iOS simulator after having launched the watch simulator in the first place.
        It looks like all the messages are queued and delivered once the app is started, but what I understood in the docs (and this tutorial and some others) the app should be activated (in the background) and receive the message immediately 🙂 thanks

  6. Hi Kristina, When sending a message from my Watch to my iPhone I am getting “Message Reply Failed” on the Watch (in the debugger). I am having no problems when I send from iPhone to the Watch. I am struggling to debug this.. any suggestions?
    Here’s the code that is causing the error:
    =======================
    // Sending data to iPhone via Interactive Messaging
    if WCSession.isSupported(){
    // we have a watch supporting iPhone

    let session = WCSession.defaultSession()

    // we can reach the watch
    if session.reachable {

    let message = [“add”: counterd]
    session.sendMessage(message,
    replyHandler: {(reply: [String : AnyObject]) -> Void in
    if let people = reply[“add”] as? Float {
    self.counter.setText(String(format:”%9.0f”, people))
    print(“Send Message Add – People \(people) “)
    }
    }, errorHandler: { (error: NSError) -> Void in
    print(“ERROR: \(error.localizedDescription)”)
    })
    }
    }
    ========================

    1. OK, Found that problem.. but I have a more fundamental question, how can I see the debug messages from the Watch and the Iphone at the same time when working in xCode? I have put logging messages in my code but I can’t seem to see the phone ones.

        1. Kristina, thanks for this help. I used the trick and was able to run thru the debugger… The strangeness is my codes seems to give me contradictory information. The debugger on the Mac (when running with the simulator) shows session.paired as false but session.reachable as true. When the message fires to send from the watch to the iPhone, I get a Payload could not be delivered message. I have tried both transferUserInfo and sendMessage. I tried posting the code at StackOverflow but got no real responses. Your blog seems to be the most effective place for getting answers on Watch OS2 programming. Any other suggestions on debugging? Thanks for an awesome site!

          1. The only thing I can think of is that there might be an issue with the payload you’re trying to send? Usually, watch error messages come with a numerical code. I would print the error message and try searching for that code number on google, or in the Apple developer forums.

      1. Hello Kristina ur suggestion worked, but i want to set text on apple watch message composer to send my message to another number. How can i do this.

        1. Hi Kristina your tutorial is really awesome, it helps me lot. I have one doubt, that how can i access iphone device current location coordinates through apple watch. If my application is running in background.

          1. If you need to do this periodically, you can wake up your iOS app with a background fetch, get the coordinates you need, and then transfer them to the watch via watch connectivity. It’s better to do these data fetches on the phone to conserve the watch’s battery. If you’re looking for a way to get location coordinates easily, here’s a shameless plug to an open source library that I contribute to https://github.com/intuit/LocationManager

          2. Thanks for reply, i m getting coordinates in apple watch, but unable to fetch coordinates in background to getting location in apple watch.

  7. Thanks for suggestion :
    I tried this code:

    NSString *phoneNumber ;
    phoneNumber=@”sms:AnyMobile No”;
    [[WKExtension sharedExtension] openSystemURL:[NSURL URLWithString:phoneNumber]];
    It is sending those message which are stored in apple watch. I want to send some alert message to particular number. Like “HELP ME!” type.

  8. Hi Kristina, this tutorial helped me a lot. I was wondering if you ever have issues where your watch interface controller sends a command via sendMessage that is not “heard” by the iOS app delegate? I have a message get sent on willActivate() and it doesn’t always hear a reply from the ios half 🙁

    1. Honestly, watch connectivity seems to be a bit finicky at the best of times. You may be having an issue with reachability. Unfortunately, it’s just a Boolean property you can check on WCSession and doesn’t have a callback alerting you to when reachability has been established between the devices. If your reachability property is false, then there’s not much you can do except try to trigger it again at a later time.

      One solution that I’ve seen people doing is to have your regular sendMessage call, along with a backup transferApplicationData call that will deliver your data as a background transfer. That way, you’ll be able to receive data eventually, even if the sendMessage call fails. If you receive both, there’s not much harm done since the payload is the same and your data will be overwritten by the same thing (unless you go the extra mile and check for duplicated data). Depends on how expensive it is to refresh your data.

  9. It is very Simple and good Example for passing data fro Watch to iOS app.
    but how to do that in iOS app to Watch?
    Please give suggestion ASAP.
    Including sample will more helpful.

Leave a Reply to gsaboGary Cancel reply

Your email address will not be published. Required fields are marked *