In the age of watchOS 1, the watchKit extension was on the paired iOS device, making sharing data between it and the main iOS app easy. For the simplest of data, like preferences, we could just use NSUserDefaults with App Groups functionality. We still should use that when sharing data between other extensions that still remain on the phone, like today view extensions, but not for watchOS apps anymore.
Luckily, Apple gave us a new API to use, that is significantly more robust than piggy-backing on App Groups, Watch Connectivity. Watch Connectivity gives a lot more information about the status of the connection between your Apple Watch and its paired iPhone. It also allows interactive messaging between the iOS App and the watchOS app, as well as background transfers that come in 3 flavors. Those flavors are:
- Application Context
- User Info Transfer
- File Transfer
Today we will be talking about the first one, Application Context.
What is Application Context
Say you have a Watch App that has some settings that can be set from the iOS app side, like whether temperature should be shown in Celsius or Fahrenheit. For settings like this, unless you expect the user to use the watchOS app immediately after doing this setting, sending the settings values over to the watch via a background transfer would make sense.
It probably isn’t IMMEDIATELY necessary, so the system might as well send it over when it would save the most battery. You also don’t need any history, since the user probably won’t care that the setting was Celsius an hour earlier.
That’s where Application Context comes in. Application Context is meant to send only the most recent data. If you set that temperature setting from Celsius to Fahrenheit, and then set it (or another setting) to a different value before the iPhone sent the Application Context over to the Watch, it would just overwrite the previous App Context that is waiting to be sent.
If you did want it to have a history of previous pieces of information sent over in a way to save the most battery, that would be what the “User Info” transfer is for. It is very similar to how you use Application Context, but it queues up the updates and sends them individually (instead of just overwriting something and only sending the most recent one). That will be the topic of another post at some point.
Setup the iOS App
We’ll start with an app similar to the one made in the previous post watchOS Hello World App in Swift. However, in this one, we will add a UISwitch to the iPhone app, and update the WKInterfaceLabel on the watchOS app to say the state of the UISwitch.
Firstly, in the iOS app’s view controller, we need to set up a few things:
import WatchConnectivity class ViewController: UIViewController, WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { } func sessionDidBecomeInactive(_ session: WCSession) { } func sessionDidDeactivate(_ session: WCSession) { } var session: WCSession? override func viewDidLoad() { super.viewDidLoad() if WCSession.isSupported() { session = WCSession.default session?.delegate = self session?.activate() } } }
So, we first need to import the WatchConnectivity framework. Without that, nothing else we do would work. Next, to respond to callbacks from the WCSession, we need to set this ViewController as the WCSession’s delegate, and to do THAT we need it to conform to the WCSessionDelegate protocol, so add that after the ViewController’s UIViewController superclass declaration.
After that, we need a few methods to conform to the WCSessionDelegate. For this app, they aren’t particularly necessary, but if would want to work with fast watch app switching, you will need to fill them out further.
Then, we should create a variable to contain the WCSession. Since its source is a singleton, we don’t technically need to, but it certainly is shorter to type “session?” than “WCSession.default” every time.
In the earliest place to run your code, you should set up that session. In most cases, that would be in an initializer, but since we are doing this in a ViewController, the earliest place we probably should go is viewDidLoad. Generally though, you should not do this in a View Controller, since your app would want to be able to update its models when that specificView Controller is not loaded or onscreen. For simplicity’s sake though, I am doing it in the ViewController just to show how to use the API. If this View Controller is the ONLY thing that cares about using the WCSession, it would be okay, but generally that isn’t the case.
To set up the session, first we check the response from WCSession’s “isSupported” method. This tells our code whether it even handles sessions. This is particularly important if this code is run on an iPad. You can’t currently pair an Apple Watch to an iPad, so this would respond with a false, and you should just not run any Watch Connectivity code at all. On the iPhone though, this would respond with true.
Once that is checked, we will store WCSession’s defaultSession there. After that, we will set this ViewController as the delegate, and activate the session. This session is being used as a constant, and if we could perform the isSupported test in an initializer, we would have set it to a constant. The session is an Optional because we don’t know if the app is being run on an iPad or not until runtime, so we will set it to the WCSession’s defaultSession if we can, or nil if not. That way, on an iPad, when we try to access properties or methods in our session, they won’t even be run because the session is nil.
Put a UISwitch on your Storyboard, and wire up its Value Changed action to the ViewController code. Inside it, we can use this code:
@IBAction func switchValueChanged(_ sender: UISwitch) { if let validSession = session { let iPhoneAppContext = ["switchStatus": sender.isOn] do { try validSession.updateApplicationContext(iPhoneAppContext) } catch { print("Something went wrong") } } }
First we are checking if we have a valid session, so that if this is run on an iPad, this whole code block will be skipped. The application context is a Swift dictionary that has a Swift String as a key, and AnyObject as the payload. These must follow the rules for property lists, and only contain certain types. Which types you can use are covered in the previous post NSUserDefaults — A Swift Introduction, because NSUserDefaults has the same limitations. Nonetheless, we are sending a Swift Bool, which will just be mapped to the NSNumber boolean value, so that’s fine.
The call do updateApplicationContext can throw an error, so we have to wrap it in a do-block and “try” the call. We’re just printing something to the console if something went wrong, but you can put whatever you need in there, like showing a UIAlertController if you need to let the user know, as well as an clean up or recovery code if it is necessary for the error. For sending the context, that’s all we need.
Set up the watchOS App
We’re starting off with the app made in the previous post watchOS Hello World App in Swift, so we have some setup already done. Similar to the iPhone, we need to perform some set up to use WatchConnectivity
import WatchConnectivity class InterfaceController: WKInterfaceController, WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { } let session = WCSession.default override func awake(withContext context: Any?) { super.awake(withContext: context) session.delegate = self session.activate() } //... }
There’s more code in here from the previous app that is being omitted, but I’m just showing the parts associated with WatchConnectivity set up. So again, we will import the WatchConnectivity framework, and have our Interface Controller conform to the WCSessionDelegate protocol. Next, we will initialize the session constant to WCSession’s defaultSession singleton.
This is different from the iOS side, because we are setting this to a constant AND a non-optional. We don’t need to do the same tests in the watchOS side, because clearly an Apple Watch running at least watchOS 2 supports Watch Connectivity. Since we are initializing it right there, and there are no other platforms (like the iPad) to worry about, we don’t need it to be optional.
Then, rather early in your code, we’ll want to set up that session. The awakeWithContext method is a pretty good place in an InterfaceController, so we’ll do it there. Like the iOS app, we’ll set this class to be the delegate, and activate the session.
To handle actually processing the Application Context let’s made a helper method, because we might want to call it more than just when we receive a new one (you’ll see why soon).
func processApplicationContext() { if let iPhoneContext = session.receivedApplicationContext as? [String : Bool] { if iPhoneContext["switchStatus"] == true { displayLabel.setText("Switch On") } else { displayLabel.setText("Switch Off") } } }
In relation to the Application Context, WCSession has 2 properties, applicationContext and receivedApplicationContext. The difference is:
- applicationContext — The most recently SENT application context from this device.
- receivedApplicationContext — The most recently RECEIVED application context by this device.
Now, seeing them both together, at least for the received, that seems obvious, but in my first foray into this (without remember everything from the WWDC Intro to Watch Connectivity Video ?), I thought that applicationContext would be updated from the most recently sent or received because I thought it would be a consistent context. I was VERY incorrect, and it took a while to realize these are separate. I definitely can see why they are, since we may send different data in each, like if this is from the Watch’s perspective, the applicationContext would be the Watch’s context that the iPhone would need, while the receivedApplicationContext would be the aspect’s of the iPhone’s context that the Watch would need. Either way though, remember that these are different, and choose the one you need.
So in this method, we will first try to cast the [String : AnyObject] dictionary to the [String : Bool] dictionary we need. If that succeeds, we then will set the displayLabel’s text to be “Switch On” or “Switch Off” depending on the state of that Boolean in the Swift Dictionary.
When we actually receive a new Application context, this Interface Controller will get a delegate callback from our WCSession object informing us to that fact, we will call this helper method from there:
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { DispatchQueue.main.async() { self.processApplicationContext() } }
Now, you probably see that didReceiveApplicationContext come’s with its own copy of the context it received. This is placed in the aforementioned receivedApplicationContext property, so we don’t need it when we call our helper method, and that’s why it doesn’t need to take any arguments.
Now, what is with that dispatch_async call? Well, these delegate callbacks are not on the Main Thread. You should never update UI in iOS or watchOS from any thread other than the Main Thread. Besides reading from the receivedApplicationContext, our helper method’s entire purpose is to update a UI element, so we will call that method by using dispatch_async to get back to the main thread. The dispatch_async call takes 2 arguments, firstly the queue you want to dispatch to (which, for the main thread, we get from the dispatch_get_main_queue method), and a closure to tell it what to do, in which we just told it to call the helper method.
So, why are we doing this in a helper method instead of right there? Well, didReceiveApplicationContext is called when you actually receive an new Application Context. It is also called shortly after calling WCSession’s activateSession method if the app received a new Application Context while it was closed. In this case though, I am using this application context as the backing store for this information. Is that a good idea? I’m not sure, but for a simple app like this, it made sense, because the whole point of that label is to say whether the Phone’s UISwitch was on or off.
So what do we do if our app is loaded and we want it to use that last received value, but there wasn’t a new one while it was closed. We call our helper method early in our view’s lifecycle to set that label, so our awakeWithContext now looks like:
override func awake(withContext context: Any?) { super.awake(withContext: context) processApplicationContext() session.delegate = self session.activate() }
Since awakeWithContext is indeed on the Main Thread, we don’t need that dispatch_async, so that is why it wasn’t in the helper method itself, and used to call the helper method from the didReceiveApplicationContext callback.
At the moment, the iOS App does not keep the state for this UISwitch, so keeping them in sync at startup is not as important, for a non-trivial app we would store the state of that UISwitch somewhere. We COULD use the applicationContext property of the WCSession on the iPhone side (remember, applicationContext is the last SENT context from the device), but what if this was run on an iPad? You could store it in NSUserDefaults, or many other places, but those are outside of the scope of how to use WatchConnectivity’s Application Context. You can read about how to use it in the earlier post NSUserDefaults — A Swift Introduction though!
The Code
For Completion’s sake, here is the whole code for this project:
ViewController.swift
import UIKit import WatchConnectivity class ViewController: UIViewController, WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { } func sessionDidBecomeInactive(_ session: WCSession) { } func sessionDidDeactivate(_ session: WCSession) { } @IBOutlet var theSwitch: UISwitch! var session: WCSession? override func viewDidLoad() { super.viewDidLoad() if WCSession.isSupported() { session = WCSession.default session?.delegate = self session?.activate() } } func processApplicationContext() { if let iPhoneContext = session?.applicationContext as? [String : Bool] { if iPhoneContext["switchStatus"] == true { theSwitch.isOn = true } else { theSwitch.isOn = false } } } @IBAction func switchValueChanged(_ sender: UISwitch) { if let validSession = session { let iPhoneAppContext = ["switchStatus": sender.isOn] do { try validSession.updateApplicationContext(iPhoneAppContext) } catch { print("Something went wrong") } } } }
InterfaceController.swift
import WatchKit import WatchConnectivity class InterfaceController: WKInterfaceController, WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { } @IBOutlet var displayLabel: WKInterfaceLabel! let session = WCSession.default override func awake(withContext context: Any?) { super.awake(withContext: context) processApplicationContext() session.delegate = self session.activate() } @IBAction func buttonTapped() { //displayLabel.setText("Hello World!") } func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { DispatchQueue.main.async() { self.processApplicationContext() } } func processApplicationContext() { if let iPhoneContext = session.receivedApplicationContext as? [String : Bool] { if iPhoneContext["switchStatus"] == true { displayLabel.setText("Switch On") } else { displayLabel.setText("Switch Off") } } } }
Remember, this is from the HelloWatch App, but we did not use the button on the watchOS app, so I just commented out its original capability.
Conclusion
And that’s how you use Watch Connectivity’s Application Context background transfer. Going the other way (back to the phone) is done pretty much the same way, with the same delegate callbacks and properties. Though in that case, depending on what you’re doing, you will probably want to check whether there actually is an Apple Watch paired with the device or if the Watch app is installed.
As I mentioned earlier, doing all of the code in your ViewController/InterfaceController may not be the best idea, but was done to simply show how the API can be used. I am a huge fan of doing it in its own Watch Connectivity manager entity, so I highly recommend checking out Natasha The Robot’s post WatchConnectivity: Say Hello to WCSession and it’s associated GitHub Gist. These really helped me when working with Watch Connectivity.
I hope you found this article helpful. If you did, please don’t hesitate to share this post on Twitter or your social media of choice, every share helps. Of course, if you have any questions, don’t hesitate to contact me on the Contact Page, or on Twitter @CodingExplorer, and I’ll see what I can do. Thanks!
Sources
- The Swift Programming Language – Apple Inc.
- Facets of Swift, Part 5: Custom Operators — Swift Programming — Medium
- watchOS 2 Tutorial: Using application context to transfer data (Watch Connectivity #2) by Kristina Thai
- WatchConnectivity: Sharing The Latest Data via Application Context by Natasha The Robot