Swift. Gg /2018/08/15/… By codingexplorer translator: khala-wan proofread: Yousanflics, wongzigii finalized: CMB

In the watchOS 1 era, WatchKit Extension was on paired iOS devices, making it easy to share data between the host APP and the Watch. The simplest data, such as preferences, is accessed through the App Groups function. Sharing data between other existing extensions on the phone and the main app should still work this way, such as the Today View Extension, but it no longer works with watchOS’s app. Fortunately, Apple has provided us with a new API to do just that. Compared with App Groups, Watch Connectivity has more powerful functions. Not only does it provide more information about the status of the connection between your Apple Watch and its iPhone companion, it also allows them to exchange messages and send them in the background in three ways:

  1. Application Context
  2. User Info Transfer
  3. File Transfer

We’ll start with the first one today: Application Context.

What is Application Context

Let’s say you have a Watch app, and it has some Settings that you can set on the iOS app side, like whether the temperature is displayed in Degrees Celsius or Fahrenheit. For such Settings, unless you want the user to use the App on the Watch immediately after setting, it is reasonable to send the information of Settings to the Watch through background transmission.

Because it may not be immediately needed, the system will probably send it out when it can save the most power. You also don’t need any history, as users may not care if they were set to degrees Celsius an hour ago.

That’s where the Application Context comes in. It is only used to send the latest data. If you change the temperature setting from Celsius to Fahrenheit, and then set it (or any other setting) to a different value before the Application Context is sent to Watch, the latest value overrides the information waiting to be sent.

If you really want it to keep a history of previous messages, and do it in the most energy-efficient way possible. Then you can use the User Info mode for transmission. It is used in a similar way to Application Context, but it adds updates to a queue and sends them one by one (rather than just overwriting something and sending the latest information). The specific use of User Info will be the subject of a future article.

Set up iOS apps

We’ll start with an App similar to the watchOS Hello World App in Swift from the previous article. However, in this article, we will add a UISwitch control to the iPhone app and illustrate the status of UISwitch by updating the WKInterfaceLabel on the watchOS app.

First, in the viewController of our iOS app, we need to set 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(a) {
        super.viewDidLoad()
        
        if WCSession.isSupported() {
            session = WCSession.defaultsession? .delegate =selfsession? .activate() } } }Copy the code

Next, we need to import the WatchConnectivity framework first. Without it, everything we do is useless. Next, in order to respond to callbacks from WCSession, we need to set the current ViewController as a proxy for WCSession, and to do that we need to make it comply with this protocol, So add the WCSessionDelegate protocol to the parent class declaration of the ViewController.

Next, we need to implement some methods in the WCSessionDelegate. They’re not particularly necessary for the current app, but you’ll need to implement them further if you want to switch quickly within the Watch App.

After that, we need to create a variable to store the WCSession. Because WCSession is actually a singleton, technically we can skip this step, but each time you enter a session, right? It’s definitely shorter than wcsession.default.

You should set sessions early in the code run. In most cases, this will be done at program initialization time. But since we’re doing this in a ViewController, the earliest place we can do it is probably in the viewDidLoad method. In general, you shouldn’t do this in a viewController, because your app wants to update its data model without loading a particular viewController on the screen. For simplicity, I did this in the viewController, just to show you how to use these apis. If this ViewController is the only thing that cares about using WCSession, it doesn’t matter. But that is not usually the case.

To set the session, we need to check whether the isSupport method of WCSession is supported or not. This is especially important if the app is running on an iPad. Currently, you can’t pair an iPad with an Apple Watch, so it returns false to indicate that WCSession is not supported on the iPad. On the iPhone it will return true.

Once we’re done checking, we can store the defaultSession of WCSession there, then set the ViewController as its proxy and activate the session. If we can test for support by executing isSupported in the initializer, we can use session as a constant. Session is optional because we don’t know if the program is going to run on iPad, so session is WCSession. Defualt when WCSession is supported, nil otherwise. That way, when we try to access properties or methods in session on the iPad, they don’t even execute because session is nil.

Place a UISwitch on the Storyboard and attach its ValueChanged method to the ViewController. Add the following code to the method:

@IBAction func switchValueChanged(_ sender: UISwitch) {
    if let validSession = session {
        let iPhoneAppContext = ["switchStatus": sender.isOn]
 
        do {
            try validSession.updateApplicationContext(iPhoneAppContext)
        } catch {
            print("Something went wrong")}}}Copy the code

First check to see if we have a valid session, and if it’s running on an iPad, skip the entire code block. Application Context is a Swift Dictionary that uses String as the key and AnyObject as the value (Dictionary

). A value must follow the rules of an attribute list and contain only certain types. It has the same limitations as NSUserDefaults, so the last article, NSUserDefaults — A Swift Introduction, showed you exactly what types you can use. However, when we send a Swift Bool, it’s going to be converted to NSNumber Boolean value, so it doesn’t matter.
,>

Calling updateApplicationContext might throw an exception, so we need to wrap it in a do-block and call it with a try. If there’s an exception, we just print a little bit of information on the console, and you can also set up whatever you need, like you might want to let the user know that there’s an error, so you can display a UIAlerController, and also, if necessary, you can put in exception cleanup or recovery code. This is all we need to prepare to send the Application Context.

Set up the watchOS application

Since we are using the Hello World App from watchOS Hello World App in Swift, some of the same Settings have already been done for us. Like the iPhone, there are some Settings we need to make 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()
    }
/ /...
}

Copy the code

Here we omit some irrelevant code from the previous App and show only the parts related to the WatchConnectivity setup. Again, we need to import the WatchConnectivity framework and have our InterfaceController comply with the WCSessionDelegate protocol. Next, We initialize the session constant to a singleton of WCSession defaultSession.

Unlike on iOS, we declare session as a constant with a non-optional value. Obviously, The Apple Watch running at least watchOS 2 supports Watch Connectivity, so we don’t need to do the same test on watchOS. And we initialized it when we declared it, and there were no other platforms (like the iPad) to worry about, so we didn’t need it to be optional.

Next, at the beginning of the code, we need to set up the session. The awakeWithContext method is a good place to be in InterfaceController, so we’re going to set that up here. As with iOS apps, we set up the current class as a proxy for the session and then activate the session.

Let’s write a helper method to handle the Application Context callback, because we might call it multiple times, not just when we receive a new Context (as you’ll see soon).

func processApplicationContext(a) {
    if let iPhoneContext = session.receivedApplicationContext as? [String : Bool] {

        if iPhoneContext["switchStatus"] = =true {
            displayLabel.setText("Switch On")}else {
            displayLabel.setText("Switch Off")}}}Copy the code

WCSession has 2 and Application Context related attributes, and receivedApplicationContext applicationContext. The differences are:

  • ApplicationContext – The last time this device was usedsendtheApplication Context.
  • Last time receivedApplicationContext – this devicereceivetheApplication Context.

Now, if you put the two together, at least the reception looks pretty obvious. But the first time I touched on this (don’t remember the entire Watch Connectivity video from WWDC?) I consider the applicationContext to be updated from the most recent sent or received, because I consider them consistent contexts. But I was so wrong, it took me a while to realize they were separate. I can certainly see why, because we might send different data every time, just like from the Watch’s point of view, applicationContext is the watch-related context that the iPhone needs, And receivedApplicationContext is the Watch as the iPhone related context. Either way, remember that they are two different things and choose the one you need based on the situation.

So in this method, we will first try to receivedApplicationContext by [String: AnyObject] type of dictionary is converted to the [String: Bool] we need type. If the conversion succeeds, the text value of the displayLabel is set to “Switch On” or “Switch Off” depending On the state of the Boolean value in the dictionary.

When we actually receive a new Application Context, the InterfaceController will receive a proxy callback from our WCSession object to notify us of this information, and we will call the helper method there.

func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
    DispatchQueue.main.async() {
        self.processApplicationContext()
    }
}
Copy the code

Now, you probably saw didReceiveApplicationContext method into the parameter with it receives the Application a copy of the Context. It is stored in the receivedApplicationContext properties mentioned above. So we don’t need it to call the helper method, so this method doesn’t need to pass in any line arguments.

, actually for processApplicationContext auxiliary method, increase the line and the context instead more functional, more Swift. By adding an entry to the context, we decouple the method’s internal implementation from its external dependencies, making it easier to unit test.

So what is dispatch_async called for? Well, these proxy callbacks are not on the main thread. You should never update the UI in iOS or watchOS on any thread other than the main thread. And our helper methods in addition to read information from the receivedApplicationContext, main purpose is used to update the UI elements. So we call this method by returning to the main thread with the dispatch_async method. Calling dispatch_async takes two arguments, first to dispatch the queue (for the main thread we get it via the dispatch_get_main_queue method) and then a closure to tell it what it needs to do, here we just tell it to call the helper method.

So, why do we do this in a helper method, rather than directly in a callback method? Well, when you actually received a new Application Context, will callback didReceiveApplicationContext proxy method. When WCSession close to receive a new ApplicationContext will call activateSession method, will soon after the callback to didReceiveApplicationContext method. In this case, I use this ApplicationContext as a backup store for this information. I’m not sure this is a good idea, but it makes sense for a simple app, since the point of the label is to show whether UISwitch is on or off on the iPhone.

So what happens when our app is loaded and wants to use the last value we received, but the app doesn’t receive a new context during shutdown? We set the label early in the view lifecycle, so now awakeWithContext should look like this:

override func awake(withContext context: Any?) {
    super.awake(withContext: context)
 
    processApplicationContext()
 
    session.delegate = self
    session.activate()
}

Copy the code

Since awakeWithContext is definitely on the main thread, we don’t need dispatch_async. So that’s how it is used only for in didReceiveApplicationContext callback to invoke the helper methods rather than in the cause of the auxiliary method for internal use.

At this point, the iOS App does not retain the state of the UISwitch, so keeping them in sync at startup is not that important. For a valuable App, we should store the state of the UISwitch somewhere. For example, you can use the ApplicationContext property of WCSession on the iPhone. (Remember, applicationContext is the last context sent from the device), but what if it’s running on an iPad? You can store it in NSUserDefaults, or any number of other places, but that’s outside the scope of the discussion of how to use WatchConnectivity. You can read about this in an earlier NSUserDefaults — A Swift Introduction.

code

Here is the complete code for the 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(a) {
        super.viewDidLoad()
        
        if WCSession.isSupported() {
            session = WCSession.defaultsession? .delegate =selfsession? .activate() } }func processApplicationContext(a) {
        if letiPhoneContext = session? .applicationContextas? [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")}}}}Copy the code

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(a) {
        //displayLabel.setText("Hello World!" )
    }
    
    func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
        DispatchQueue.main.async() {
            self.processApplicationContext()
        }
    }
    
    func processApplicationContext(a) {
        if let iPhoneContext = session.receivedApplicationContext as? [String : Bool] {
            
            if iPhoneContext["switchStatus"] = =true {
                displayLabel.setText("Switch On")}else {
                displayLabel.setText("Switch Off")}}}}Copy the code

Remember that this code comes from the Hello World App, but we didn’t use the Button on the watchOS App. So I just commented out the code for the original functionality.

conclusion

This is how to use Watch Connectivity Application Context for background transmission. Sending back data to the phone is exactly the same because they have the same proxy callback and properties. In that case, though, you might want to check to see if there’s an Apple Watch that works with the device or if the Watch has an app installed.

As I mentioned earlier, executing all the code in ViewController/InterfaceController is probably not the best idea, but this is just a simple demonstration of how to use the API. I personally enjoy doing this in my Watch Connectivity Manager instance. So I strongly encourage you to read Natasha The Robot’s article WatchConnectivity: Say Hello to WCSession and associate it with his GitHub Gist. This will help you use WatchConnectivity.

I hope you found this article helpful. If it helps, please don’t hesitate to share this post on Twitter or social media of your choice, every share helps me. Of course, if you have any questions, please feel free to contact me via the contact page or on Twitter @codingExplorer and I’ll see what I can do. Thank you very much!

source

  • 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