7. Interfacing with iOS Apps

For the past 33 years, I have looked in the mirror every morning and asked myself: “If today were the last day of my life, would I want to do what I am about to do today?” And whenever the answer has been “No” for too many days in a row, I know I need to change something.

Steve Jobs

In watchOS 2, Apple has changed the execution model of the Apple Watch app. Instead of the logic of the watch app being executed on the iPhone, in watchOS 2 the logic is run on the watch itself. While this change is much welcomed (as the performance of apps is drastically improved), it presents another challenge to the developer: How do you communicate between the containing iOS app and the watch app? In watchOS 1, you communicate using the shared app group feature; however, this feature is no longer relevant in the new execution model of watchOS 2. Fortunately, WatchKit for watchOS 2 comes with a new framework: the Watch Connectivity Framework, which contains a set of APIs that allow the containing iOS app to communicate with the watch app (and vice versa). That is the subject of this chapter.

In addition to discussing how apps intercommunicate, this chapter also talks about how to use location services in your watch app, as well as how to consume web services. Last, but not least, this chapter ends with a discussion on persisting data on your watch app.

Introducing the Watch Connectivity Framework

In Chapter 1, “Getting Started with WatchKit Programming,” you learned that to communicate between the containing iOS app and the Apple Watch app, you make use of the Watch Connectivity Framework. The Watch Connectivity Framework provides a two-way communication channel between the iOS app and the watch app. Using this framework, you can perform two key types of communication:

Image Background transfers: Data/files are sent to the recipient in the background and are available when the recipient app launches.

Image Live communication: Data is sent directly to the recipient app, which is currently active or launched to receive the data.


Note

Recipient here refers to either the iOS app or the Apple Watch app.


The next section elaborates on these two types of communication.

Types of Communication

For background transfers, the Watch Connectivity Framework supports three communication modes:

Image Application Context

Image User Info

Image File Transfer

For the Application Context mode, you can send the most recent state information to the recipient. Figure 7.1 shows an example where the iOS app sends a series of dictionaries (a, b, and c) to the Apple Watch. However, only the most recently sent dictionary is delivered to the watch app when it launches. This mode is useful for updating the state of your application, such as updating glances to display the most recent information about your app.

Image

Figure 7.1 Sending data using the Application Context mode


Note

Chapter 9, “Displaying Glances,” contains an example of using the Application Context mode to send glance information.



Note

Note that the Watch Connectivity Framework is bidirectional—you can send data from the iPhone app to the Apple Watch app, and vice versa.


For the User Info mode, all data (dictionaries) is delivered in the same order in which it was sent (see Figure 7.2). This mode is useful if you need to continually send a series of data to the recipient (for example, sending a series of location coordinates to the iPhone for logging purposes).

Image

Figure 7.2 Sending data using the User Info mode

For the File Transfer mode, you can transfer a file together with an optional dictionary to the recipient (see Figure 7.3). The file is transferred asynchronously in the background and saved in the ~/Documents/Inbox directory of the recipient. Once the recipient has been notified of an incoming file, the file is deleted automatically. This mode is useful for cases where you need to transfer files such as images to the recipient.

Image

Figure 7.3 Sending files using the File Transfer mode

Besides the various background transfer modes, the Watch Connectivity Framework also supports live communication. For live communication, it supports the Send Message mode. This mode requires apps on both devices to be running. You can send data to the recipient and optionally request a reply from the recipient (see Figure 7.4).

Image

Figure 7.4 Sending live data using the Send Message mode

In the event the recipient is not running when you send data using the Send Message mode, the following happens:

Image If you are sending data from the watch app, it triggers the corresponding iPhone app and launches it in the background to receive the data.

Image If you are sending data from the iPhone app, it returns with an error indicating that the recipient is not reachable. You need to launch the watch app in order to receive the data.

Using the Watch Connectivity Framework

Now that you have a better understanding of how the Watch Connectivity Framework works, it is time to put it into action:

1. Using Xcode, create a new iOS App with WatchKit App project and name it Communications. Uncheck the option Include Notification Scene so that we can keep the WatchKit project to a bare minimum.

2. In the Main.storyboard file in the iOS project, add a TextView to the View window (for simplicity I have set this to the size of a 4.7-inch iPhone) and then set its Background attribute to Group Table View Background Color (see Figure 7.5). The TextView will be used to display all the data received from the Apple Watch.

Image

Figure 7.5 Populating the View window

3. Create an outlet for the TextView in ViewController.swift and connect it to the TextView:

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var textView: UITextView!

Sending Data Using Application Context

The first example shows how to send data from the Apple Watch to the containing iOS app using the Application Context mode.

In the WatchKit Extension

In the following steps, you modify the WatchKit Extension to send data from the Apple Watch app to the iOS app.

1. Add the following statements in bold to the InterfaceController.swift file:

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)

        // Configure interface objects here.

        if (WCSession.isSupported()) {
            let session = WCSession.defaultSession()
            session.delegate = self
            session.activateSession()

            //---on the watch, reachable is true if the iPhone is reachable
            // via Bluetooth---
            print("iPhone reachable: (session.reachable)")
        }
    }

By adding these statements, you

Image Imported the WatchConnectivity framework to the project.

Image Made the InterfaceController class implement the WCSessionDelegate protocol. This protocol contains various methods that are fired when the watch receives incoming data from the iPhone.

Image Created a session using the defaultSession method from the WCSession class. You then set its delegate property so that it knows the ViewController class will handle the methods fired by the WCSession class. When you are ready to receive incoming data, you call the activateSession method.

Image Used the reachable property to check if the iPhone is reachable. On the Apple Watch, this property returns true if the iPhone is reachable via Bluetooth.


Note

Whenever there is a change in reachability between the iPhone and the Apple Watch, the WCSession object fires the sessionReachabilityDidChange: method. You can implement this method to be notified of such a change.


2. In the Interface.storyboard file, add a Button control to the Interface Controller and then set its title to Application Context (see Figure 7.6).

Image

Figure 7.6 Populating the Interface Controller

3. Create an action for the button in the InterfaceController.swift file:

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {

    @IBAction func btnAppContext() {
    }

4. Add the following statements in bold to the InterfaceController.swift file:

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {

    @IBAction func btnAppContext() {
        do {
            let applicationContext =
                ["key1":"watch",
                 "key2":"appcontext",
                 "time": "(NSDate())"]
            try
                WCSession.defaultSession().updateApplicationContext(
                    applicationContext)
        } catch {
            print("(error)")
        }
    }

In these statements, you

Image Created a dictionary containing three keys: key1, key2, and time

Image Used the updateApplicationContext: method of the WKSession object to send the dictionary to the iPhone

In the iOS App

In the following, you modify the iOS app to receive data from the Apple Watch app. Add the following statements in bold to the ViewController.swift file:

import UIKit

import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate {

    @IBOutlet weak var textView: UITextView!

    func updateTextView(message:String) {
        dispatch_async(dispatch_get_main_queue()) {
            self.textView.text =  message + " " + self.textView.text
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a
        // nib.

        //---clear the TextView---
        textView.text = ""

        if (WCSession.isSupported()) {
            let session = WCSession.defaultSession()
            session.delegate = self
            session.activateSession()

            //---on the iPhone, reachable is true only if the watch is
            // reachable via Bluetooth and the watch app is in the
            // foreground---
            updateTextView("Apple Watch app reachable: " +
                "(session.reachable)")
            updateTextView("Apple Watch paired: " +
                "(session.paired)")
            updateTextView("Watch app installed: " +
                "(session.watchAppInstalled)")
        }
    }

    func session(session: WCSession,
    didReceiveApplicationContext applicationContext: [String : AnyObject]) {
        updateTextView(applicationContext["key1"]! as! String)
        updateTextView(applicationContext["key2"]! as! String)
        updateTextView(applicationContext["time"]! as! String)
    }

By adding these statements, you

Image Imported the WatchConnectivity framework to the project.

Image Made the ViewController class implement the WCSessionDelegate protocol. This protocol contains various methods that are fired when the phone receives incoming data from the watch.

Image Added a method named updateTextView to append data in the TextView. All data is added using the main thread. This is needed as all the methods in the WCSessionDelegate protocol are not called from the main thread. Hence, you need to use the dispatch_async method to update the TextView in the main thread.

Image Used the WCSession’s isSupported method to check if the Watch Connectivity Framework is supported.

Image Created a session using the defaultSession method from the WCSession class. You then set its delegate property so that it knows the ViewController class will handle the methods fired by the WCSession class. When you are ready to receive incoming data, you called the activateSession method.

Image Used the reachable property to check if the Apple Watch is reachable. On the iPhone, this property returns true if the Apple Watch is reachable via Bluetooth and the watch app is running in the foreground.

Image Used the paired property to check if the Apple Watch is paired.

Image Used the watchAppInstalled property to check if the watch app is installed on the Apple Watch.

Image Implemented the session:didReceiveApplicationContext: method. This method is called when the watch app sends context data to the iPhone. The context data is sent as a dictionary.

Testing the Applications

You are now ready to test the application:

1. Select the Communications WatchKit App scheme in Xcode and deploy the projects onto the iPhone and Apple Watch Simulators.

2. On the Apple Watch Simulator, click the Application Context button a couple of times. This proves that the iPhone will receive only the latest dictionary that was sent; all earlier dictionaries will be replaced by the last dictionary sent.

3. On the iPhone Simulator, launch the Communications application that has been installed on it. You should see something similar to Figure 7.7.

Image

Figure 7.7 The iOS app receiving the data sent using the Application Context mode

Observe that on the iPhone, the iOS app prints out the values of the three keys: key1, key2, and time. All these keys are from the latest dictionary sent to the iPhone.


Note

In this mode, the previous dictionaries sent are always overridden by the latest one.


Sending Data Using User Info

The second example shows how you send data using the User Info mode.

In the WatchKit Extension

In the following steps, you modify the WatchKit Extension to send data from the Apple Watch app to the iOS app.

1. In the Interface Controller in the Interface.storyboard file, add a Button control (see Figure 7.8) and set its title to User Info.

Image

Figure 7.8 Populating the Interface Controller

2. Create an action for the button in the InterfaceController.swift file and connect it to the button:

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {

    @IBAction func btnUserInfo() {
    }

3. Add the following statements in bold to the InterfaceController.swift file:

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {

    @IBAction func btnUserInfo() {
        let userInfo = [
            "key1": "watch",
            "key2":"userinfo",
            "time": "(NSDate())"
        ]
        let infoTransfer =
            WCSession.defaultSession().transferUserInfo(userInfo)
        print("Transferring: (infoTransfer.transferring)")
    }

    func session(session: WCSession,
        didFinishUserInfoTransfer userInfoTransfer:
        WCSessionUserInfoTransfer,
        error: NSError?) {
        if error == nil {
            print("Transfer completed")
        } else {
            print("(error)")
        }
    }

In the previous statements, you

Image Created a dictionary containing three keys: key1, key2, and time.

Image Used the transferUserInfo: method of the WKSession object to send the dictionary to the iPhone. This method returns a WCSessionUserInfoTransfer object, which you can use to check the status of the transfer (via the transferring property).

Image Implemented the session:didFinishUserInfoTransfer:error: method. This method is fired when the transfer is completed, or if an error occurred during the transfer process.

In the iOS App

Here you modify the iOS app to receive data from the Apple Watch app. Add the following statements in bold to the ViewController.swift file:

import UIKit

import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate {

    @IBOutlet weak var textView: UITextView!

    func session(session: WCSession,
    didReceiveApplicationContext applicationContext: [String : AnyObject]) {
        ...
    }

    func session(session: WCSession,
    didReceiveUserInfo userInfo: [String : AnyObject]) {
        updateTextView(userInfo["key1"]! as! String)
        updateTextView(userInfo["key2"]! as! String)
        updateTextView(userInfo["time"]! as! String)
    }

The session:didReceiveUserInfo: method is fired when the user info data is received from the watch. The incoming data is passed as a dictionary.

Testing the Applications

You are now ready to test the application:

1. Select the Communications WatchKit App scheme in Xcode and deploy the projects onto the iPhone and Apple Watch Simulators.

2. On the Apple Watch Simulator, click the User Info button a couple of times. This proves that all data sent via this method is queued for delivery to the iPhone.

3. On the iPhone Simulator, launch the Communications application that has been installed on it. You should see something similar to Figure 7.9. Observe that you will receive multiple sets of the data (one for each click on the User Info button).

Image

Figure 7.9 Receiving the data sent using the User Info mode


Note

Dictionaries sent using this method are delivered in the order in which they are sent.


Sending Data/Files Using File Transfer

In the third example, you transfer files.

In the WatchKit Extension

In the following steps, you modify the WatchKit Extension to send data from the Apple Watch app to the iOS app:

1. Drag and drop an image named emoji.png onto the Communications WatchKit Extension target (see Figure 7.10).

Image

Figure 7.10 Adding an image to the Extension

2. In the Interface Controller in the Interface.storyboard file, add a Button control (see Figure 7.11) and set its title to File Transfer.

Image

Figure 7.11 Populating the Interface Controller

3. Create an action for the button in the InterfaceController.swift file:

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {

    @IBAction func btnFileTransfer() {
    }

4. Add the following statements in bold to the InterfaceController.swift file:

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {

    @IBAction func btnFileTransfer() {
        let filePathURL:NSURL =
            NSURL(fileURLWithPath:
                NSBundle.mainBundle().pathForResource(
                    "emoji",
                    ofType: "png")!)

        let data = [
            "key1": "watch",
            "key2":"filetransfer",
            "time": "(NSDate())"
        ]
        let fileTransfer = WCSession.defaultSession().transferFile(
            filePathURL, metadata:data)
        print("Transferring: (fileTransfer.transferring)")
    }

    func session(session: WCSession,
    didFinishFileTransfer fileTransfer: WCSessionFileTransfer,
    error: NSError?) {
        if error == nil {
            print("Transfer completed")
        } else {
            print("(error)")
        }
    }

In the previous statements, you

Image Created a path to point to the emoji.png file located in the Extension bundle.

Image Created a dictionary containing three keys: key1, key2, and time.

Image Used the transferFile: method of the WKSession object to send the image and dictionary to the iPhone. This method returns a WCSessionFileTransfer object, which you can use to check the status of the transfer (via the transferring property).

Image Implemented the session:didFinishFileTransfer:error: method. This method is fired when the transfer is completed, or if an error occurred during the transfer process.

In the iOS App

In the following steps, you modify the iOS app to receive data from the Apple Watch app:

1. In the Main.storyboard file, add an ImageView (see Figure 7.12) to the View window and set its Mode attribute to Aspect Fit.

Image

Figure 7.12 Adding the ImageView to the View window

2. Create an outlet for the ImageView in the ViewController.swift file and connect it to the ImageView:

import UIKit

import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var image: UIImageView!

3. Add the following statements in bold to the ViewController.swift file:

import UIKit

import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var image: UIImageView!
    func updateImageView(image:UIImage!) {
        dispatch_async(dispatch_get_main_queue()) {
            self.image.image = image
        }
    }

    func session(session: WCSession, didReceiveFile file: WCSessionFile) {
        let fileURL = file.fileURL
        //---move the file to somewhere more permanent as it will be deleted
        // after this method returns---
        if file.metadata != nil {
            updateImageView(UIImage(data: NSData(contentsOfURL: fileURL)!))
            updateTextView(file.metadata!["key1"] as! String)
            updateTextView(file.metadata!["key2"] as! String)
            updateTextView(file.metadata!["time"] as! String)
        }
    }

In this step, you

Image Added a method named updateImageView: to display an image in the ImageView. This is necessary, as all the methods in the WCSessionDelegate protocol are not called from the main thread. Hence, you need to use the dispatch_async method to update the ImageView in the main thread.

Image Implemented the session:didReceiveFile: method. This method is called when the watch app sends a file (with an optional dictionary) to the iPhone. The file is made available through the WCSessionFile object. You can load it via its fileURL property.


Note

The file that is transferred into the current device will be deleted after the session:didReceiveFile: method exits. Hence, you need to move it to somewhere more permanent (such as the Documents folder) if you need to use it later on.


Testing the Application

You are now ready to test the application:

1. Select the Communications WatchKit App scheme in Xcode and deploy the projects onto the iPhone and Apple Watch Simulators.

2. On the Apple Watch Simulator, click the File Transfer button a couple of times. This proves that all data sent via this method is queued for delivery to the iPhone.

3. On the iPhone Simulator, launch the Communications application that has been installed on it. You should see something similar to Figure 7.13. Observe that you will receive multiple sets of the data (one for each click on the File Transfer button).

Image

Figure 7.13 Receiving the file and the data sent using the File Transfer mode

Canceling Outstanding Transfers

The WKSession object allows you to obtain a list of outstanding File Transfer and User Info transfers:

        let outstandingFileTransfers =
            WCSession.defaultSession().outstandingFileTransfers
        let outstandingUserInfoTransfers =
            WCSession.defaultSession().outstandingUserInfoTransfers

The outstandingFileTransfers and outstandingUserInfoTransfers methods return an array of WKSessionFileTransfer and WKSessionUserInfoTransfer objects, respectively. Using this array of objects, you can cancel the transfer using the cancel method:

        for transfer in outstandingFileTransfers {
            transfer.cancel()
        }
        for transfer in outstandingUserInfoTransfers {
            transfer.cancel()
        }

Using Interactive Messaging

In this final example, you implement the Interactive Messaging mode.

In the WatchKit Extension

In the following steps, you modify the WatchKit Extension to send data from the Apple Watch app to the iOS app:

1. In the Interface.storyboard file, add a Button control to the Interface Controller and set its title to Send Message (see Figure 7.14).

Image

Figure 7.14 Populating the Interface Controller

2. Create an action for the button in the InterfaceController.swift file and connect it to the button:

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {

    @IBAction func btnSendMessage() {
    }

3. Add the following statements in bold to the InterfaceController.swift file:

import WatchKit
import Foundation

import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {

    @IBAction func btnSendMessage() {
        let message = [
            "key1":"watch",
            "key2":"sendmessage",
            "time": "(NSDate())"
        ]

        //---send a message to the iPhone and wait for a reply---
        WCSession.defaultSession().sendMessage(message,
            replyHandler: { (replies) -> Void in
                print(replies["key1"])
                print(replies["key2"])
                print(replies["time"])
            }) { (error) -> Void in
                print("(error)")
        }
    }

In these statements, you

Image Created a dictionary containing three keys: key1, key2, and time.

Image Used the sendMessage: method to send a dictionary to the iPhone. You specified a replyHandler closure, which will be executed asynchronously in a background thread. This closure is called when the recipient returns the data back to the current device. If you don’t need a reply from the recipient, you can simply set the replyHandler to nil, like this:

        //---send a message to the iPhone---
        WCSession.defaultSession().sendMessage(message,
        replyHandler: nil)
            { (error) -> Void in
                print("(error)")
        }

In the iOS App

In the following, you modify the iOS app to receive data from the Apple Watch app. Add the following statements in bold to the ViewController.swift file:

import UIKit

import WatchConnectivity

class ViewController: UIViewController, WCSessionDelegate {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var image: UIImageView!

    //---when a message arrives from the watch and needs a response---
    func session(session: WCSession,
    didReceiveMessage message: [String : AnyObject],
    replyHandler: ([String : AnyObject]) -> Void) {
        updateTextView("session:didReceiveMessage:replyHandler:")
        updateTextView(message["key1"]! as! String)
        updateTextView(message["key2"]! as! String)
        updateTextView(message["time"]! as! String)

        let reply = [
            "key1":"phone",
            "key2":"replymessage",
            "time": "(NSDate())"
        ]
        replyHandler(reply)
    }

    //---when a message arrives from the watch and no response is needed---
    func session(session: WCSession,
    didReceiveMessage message: [String : AnyObject]) {
        updateTextView("session:didReceiveMessage:")
        updateTextView(message["key1"]! as! String)
        updateTextView(message["key2"]! as! String)
        updateTextView(message["time"]! as! String)
    }

In these statements, you

Image Implemented the session:didReceiveMessage:replyHandler: method. This method is called when the watch app sends a message to the iPhone with the replyHandler set to a closure. To reply to the sender, simply pass a dictionary to the replyHandler.

Image Implemented the session:didReceiveMessage: method. This method is called when the watch app sends a message to the iPhone with the replyHandler set to nil.

Testing the Application

You are now ready to test the application:

1. Select the Communications WatchKit App scheme in Xcode and deploy the projects onto the iPhone and Apple Watch Simulators.

2. On the Apple Watch Simulator, click the Send Message button. Observe the result printed in the Output window (see Figure 7.15). The iPhone application is launched in the background, and after receiving the data from the watch, it sends back a reply.

Image

Figure 7.15 Sending a message to the iOS app and getting a reply from it

Comparing the Different Modes

Now that you have seen the various communication modes in action, let’s summarize their characteristics and differences. Table 7.1 summarizes the various key points of the four different modes of communication.

Image
Image

Table 7.1 Comparing the Four Different Communication Modes

Connecting to the Outside World

One of the key features of making a mobile app is the ability to connect to the outside world and consume external data, and the Apple Watch app is no exception. In this section, you learn how to obtain location data on the Apple Watch as well as consume web services.

Getting Location Data

In watchOS 2, the CLLocationManager class has a new method called requestLocation. Instead of getting a continuous stream of locations, the requestLocation method requests a single location update. The following example shows how you can use it to obtain the user’s location.


Note

The CLLocationManager class in watchOS does not support the startUpdatingLocation method, which is commonly used in the iOS platform.


1. Using Xcode, create a new iOS App with WatchKit App project and name it UsingLocation. Uncheck the option Include Notification Scene so that we can keep the WatchKit project to a bare minimum.

2. In the Info.plist file from the UsingLocation project (the iPhone app), add a new key and set its value as shown in Figure 7.16. The value of the NSLocationWhenInUseUsageDescription key is displayed to the user when the iPhone app requests permission to use his or her location.

Image

Figure 7.16 Adding a new key to the Info.plist file

3. Add the following statements in bold to the AppDelegate.swift file:

import UIKit
import CoreLocation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    var lm: CLLocationManager!

    func application(application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) ->
    Bool {
        // Override point for customization after application launch.

        lm = CLLocationManager()
        lm.requestWhenInUseAuthorization()

        return true
    }

The requestWhenInUseAuthorization method requests permission to use the location services whenever the application is in the foreground. This call is necessary in order for the Apple Watch to obtain the user’s current location.

4. In the Interface.storyboard file, add the following controls onto the Interface Controller (see Figure 7.17):

Image Label

Image Button

Image

Figure 7.17 Populating the Interface Controller

5. In the InterfaceController.swift file, create the following outlets and actions and connect them to the controls in the Interface Controller:

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController {

    @IBOutlet var label: WKInterfaceLabel!
    @IBAction func btnWhereAmI() {
    }

6. Add the following statements in bold to the InterfaceController.swift file:

import WatchKit
import Foundation
import CoreLocation

class InterfaceController: WKInterfaceController, CLLocationManagerDelegate {
    var lm: CLLocationManager!
    @IBOutlet var label: WKInterfaceLabel!

    @IBAction func btnWhereAmI() {
        lm.requestLocation()
    }

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)

        // Configure interface objects here.
        lm = CLLocationManager()
        lm.delegate = self
        lm.desiredAccuracy = kCLLocationAccuracyBest
    }

    func locationManager(manager: CLLocationManager,
    didUpdateLocations locations: [CLLocation]) {
        //---get the most recent location---
        let currentLocation = locations.last!
        let str = "(currentLocation.coordinate.latitude), " +
                  "(currentLocation.coordinate.longitude)"
        print(str)
    }

    func locationManager(manager: CLLocationManager,
    didChangeAuthorizationStatus status: CLAuthorizationStatus) {
        switch status.rawValue {
            case 0: label.setText("[NotDetermined]")
            case 1: label.setText("[Restricted]")
            case 2: label.setText("[Denied]")
            case 3: label.setText("[AuthorizedAlways]")
            case 4: label.setText("[AuthorizedWhenInUse]")
            default:break
        }
    }

    func locationManager(manager: CLLocationManager,
    didFailWithError error: NSError) {
        print("(error)")
    }

In the previous statements, you

Image First created an instance of the CLLocationManager class and set the delegate to self so that you can implement methods fired by the CLLocationManager class.

Image Implemented the locationManager:didUpdateLocations: method. This method is fired whenever the CLLocationManager class manages to find your latest location.

Image Implemented the locationManager:didFailWithError: method. This method is fired whenever the CLLocationManager class is not able to find the current location.

Image Implemented the locationManager:didChangeAuthorizationStatus: method. This method is fired whenever the status of the location authorization has changed.

Image Used the requestLocation method to request the location manager to return the current location (single location). Once the location is obtained (in the locationManager:didUpdateLocations: method), you print out the location.

7. Select the UsingLocation WatchKit App scheme in Xcode and deploy the projects onto the iPhone and Apple Watch Simulators. On the Apple Watch Simulator, you see that the location authorization status is not determined yet (see Figure 7.18). This is because you have not given the authorization on the iPhone app.

Image

Figure 7.18 The location authorization status is not determined yet

8. On the iPhone Simulator, launch the UsingLocation application that has been installed on it. You will now be asked for the permission (see Figure 7.19). Click Allow.

Image

Figure 7.19 Giving permission on the iOS app

Once permission is granted to access the location, you see that the Apple Watch displays the authorization as AuthorizedWhenInUse (see Figure 7.20).

Image

Figure 7.20 Once permission is granted on the iPhone, the authorization status changes on the watch app

9. With the iPhone Simulator selected, go to Debug | Location and select Freeway Drive (see Figure 7.21). This simulates the iPhone getting a series of locations.

Image

Figure 7.21 Simulating location changes on the iPhone Simulator

10. On the Apple Watch Simulator, click the Where am I? button. You should see the latitude and longitude displayed in the Output window (see Figure 7.22).

Image

Figure 7.22 Displaying the location obtained in the Output window

Display Map

Knowing the latitude and longitude of your current location is not very helpful. A better way to represent the current location would be to display it on a map. This section shows you how to display a map using the Map control:

1. In the Interface.storyboard file, add the Map control onto the Interface Controller (see Figure 7.23).

Image

Figure 7.23 Populating the Interface Controller

2. In the InterfaceController.swift file, create the following outlets and actions and connect them to the controls in the Interface Controller:

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController {

    @IBOutlet var label: WKInterfaceLabel!
    @IBOutlet var map: WKInterfaceMap!

3. Add the following statements in bold to the InterfaceController.swift file:

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)

        // Configure interface objects here.
        lm = CLLocationManager()
        lm.delegate = self
        lm.desiredAccuracy = kCLLocationAccuracyBest
        map.setHidden(true)
    }
    func locationManager(manager: CLLocationManager,
    didUpdateLocations locations: [CLLocation]) {
        //---get the most recent location---
        let currentLocation = locations.last! as CLLocation
        let str = "(currentLocation.coordinate.latitude), " +
                  "(currentLocation.coordinate.longitude)"
        print(str)

        //---for displaying the map---
        let coordinateSpan =  MKCoordinateSpan(
            latitudeDelta: 0.010, longitudeDelta: 0.010)

        let loc = CLLocationCoordinate2D(
            latitude: currentLocation.coordinate.latitude,
            longitude: currentLocation.coordinate.longitude)

        map.setHidden(false)
        map.removeAllAnnotations()
        map.addAnnotation(loc, withPinColor: WKInterfaceMapPinColor.Purple)
        map.setRegion(MKCoordinateRegion(center: loc, span: coordinateSpan))
    }

In these statements,

Image You hid the map initially when the app was loaded.

Image Once the location was obtained (in the locationManager:didUpdateLocations: method), you displayed the location using the Map control.

Image You added an annotation (a pushpin) to indicate the current location.

4. Select the UsingLocation WatchKit App scheme in Xcode and deploy the projects onto the iPhone and Apple Watch Simulators.

5. On the Apple Watch Simulator, click the Where am I? button. After a while, a map is shown with the location marked (see Figure 7.24).

Image

Figure 7.24 Displaying the location using a Map


Note

The Map control displays a static map; you won’t be able to pan the map. However, if you click on the Map control, you will be brought to another screen where you can get directions for walking or driving (see Figure 7.25, left). Scrolling to the bottom of the screen reveals the map (see Figure 7.25, center). Clicking on the map displays the built-in Maps app (see Figure 7.25, right), where you can pan the map.

Image

Figure 7.25 Clicking on the Map control launches the Maps application on the Apple Watch simulator, where you can pan the map


Accessing Web Services

In watchOS 2, because the Watch app runs directly on the Apple Watch, your app can now directly connect to the Internet without going through the iPhone. This is useful in situations where your watch is not within range of your iPhone, and this ability allows your application to remain connected with the outside world.

1. Using Xcode, create a new iOS App with WatchKit App project and name it WebServices. Uncheck the option Include Notification Scene so that we can keep the WatchKit project to a bare minimum.

2. Select the Interface.storyboard file to edit it in the Storyboard Editor.

3. Drag and drop a Button control (and set its title to Get Weather) onto the Interface Controller, as shown in Figure 7.26.

Image

Figure 7.26 Populating the Interface Controller

4. Create an action for the button in the InterfaceController.swift file:

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController {

    @IBAction func btnGetWeather() {
    }

5. Add the following statements in bold to the InterfaceController.swift file:

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController {
    let INVALID_TEMP:Double = 9999
    //---parse the JSON string---
    func parseJSONData(data: NSData) -> Double {
                var parsedJSONData: NSDictionary!
        do {
             parsedJSONData = try NSJSONSerialization.JSONObjectWithData(data,
                options: NSJSONReadingOptions.MutableContainers) as!
                NSDictionary
        } catch {
            return INVALID_TEMP
        }

        let main = parsedJSONData["main"] as? NSDictionary
        if let temp = main {
            //---convert temperature to Celsius---
            return (temp["temp"] as! Double) - 273;
        } else {
            return INVALID_TEMP
        }
    }

    func displayAlert(title:String, message:String) {
        let okAction = WKAlertAction(title: "OK",
            style: WKAlertActionStyle.Default) { () -> Void in
                print("OK")
        }
        self.presentAlertControllerWithTitle(title,
            message: message,
            preferredStyle: WKAlertControllerStyle.Alert,
            actions: [okAction])
    }

    @IBAction func btnGetWeather() {
        /*
        For http://, you need to add the following keys in Info.plist:
            NSAppTransportSecurity
                NSAllowsArbitraryLoads - YES
            Application Transport Security has blocked a cleartext HTTP
            (http://) resource load since it is insecure. Temporary exceptions
            can be configured via your app's Info.plist file.
        */
        let country = "Amsterdam"

        //---URL of the web service---
        let urlString = "http://api.openweathermap.org/data/2.5/weather?q=" +
            country

        let session = NSURLSession.sharedSession()

        session.dataTaskWithURL(NSURL(string:urlString)!,
            completionHandler: {
                (data, response, error) -> Void in
                let httpResp:NSHTTPURLResponse! = response as?
                    NSHTTPURLResponse
                if httpResp != nil {
                    if error == nil && httpResp.statusCode == 200 {
                        //---parse the JSON result---
                        let temp = self.parseJSONData(data!)
                        if temp < self.INVALID_TEMP {
                            self.displayAlert("Weather",
                                message:
                           "Weather in (country) is (temp) degrees Celsius")
                        } else {
                            self.displayAlert("Weather",
                                message: "No weather found")
                        }
                    } else {
                        self.displayAlert("Error",
                            message: "(error)")
                    }
                } else {
                    self.displayAlert("Error",
                        message: "Unable to contact web service")
                }
        }).resume()
    }

In the previous statements, you

Image Defined a function named parseJSONData: to take in an argument of type NSData (containing JSON content) and then extracted the temperature from it and returned the temperature as a double value

Image Defined a function named displayAlert: to display an alert with the specified title and message

Image Used the NSURLSession object to connect to a web service so that you can fetch the temperature of a city

6. In the Info.plist file in the Extension project, add the two keys NSAppTransportSecurity and NSAllowsArbitraryLoads, as shown in Figure 7.27.

Image

Figure 7.27 Adding the keys in Info.plist to allow access to web resources using http://

These two keys are needed so that your Watch app can connect to a web service using http:// instead of https:// (which is the default).

7. Select the WebServices WatchKit App scheme in Xcode and deploy the projects onto the iPhone and Apple Watch Simulators.

8. On the Apple Watch Simulator, click the Get Weather button. After a while, an alert appears, displaying the temperature of the specified city (see Figure 7.28).

Image

Figure 7.28 Getting the weather information


Note

To prove that the Apple Watch app can continue to connect to the web service without the iPhone, deploy the application onto a real Apple Watch and iPhone. After that, turn on flight mode on the iPhone and run the application on the Apple Watch. You should still be able to get the weather of the specified city.


Saving Data

In this section, we briefly discuss two techniques for persisting data in your Apple Watch applications. First, you can write to a text file and save it into the Documents directory on your Apple Watch. Second, you can make use of the NSUserDefaults class to save key/value pairs.

Creating the Project

Let’s create a project that prompts the user to enter a string (or select from a list of predefined strings):

1. Using Xcode, create a new iOS App with WatchKit App project and name it FileStorage. Uncheck the option Include Notification Scene so that we can keep the WatchKit project to a bare minimum.

2. Select the Interface.storyboard file to edit it in the Storyboard Editor.

3. Drag and drop the following controls onto the Interface Controller, as shown in Figure 7.29:

Image Three buttons

Image Label

Image Image

Image

Figure 7.29 Populating the Interface Controller

4. Create the following outlets and actions in the InterfaceController.swift file:

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController {

    @IBOutlet var image: WKInterfaceImage!
    @IBOutlet var label: WKInterfaceLabel!

    @IBAction func btnInputText() {
    }

    @IBAction func btnSave() {
    }

    @IBAction func btnLoad()  {
    }

Writing to Files

There are a few different methods you can use to save data to files. The first method is to write data to files. The String structure has a couple of methods that allow you to save the contents of the string into a file as well as load its contents from a file:

1. Add the following statements in bold to the InterfaceController.swift file:

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController {

    var filePath:String!
    var symbol:String! = "[Symbol]"

    @IBOutlet var image: WKInterfaceImage!
    @IBOutlet var label: WKInterfaceLabel!

    func displayAlert(title:String, message: String) {
        let okAction = WKAlertAction(title: "OK",
            style: WKAlertActionStyle.Default) { () -> Void in
                print("OK")
        }
        presentAlertControllerWithTitle(title,
            message: message,
            preferredStyle: WKAlertControllerStyle.Alert,
            actions: [okAction])
    }

    @IBAction func btnInputText() {
        presentTextInputControllerWithSuggestions(
            ["AAPL", "AMZN", "FB", "GOOG"],
            allowedInputMode: WKTextInputMode.AllowEmoji)
            { (results) -> Void in
                if results != nil {
                    //---trying to see if the result can be converted
                    // to String---
                    self.symbol = results!.first as? String
                    if self.symbol != nil {
                        self.label.setText(self.symbol!)
                    }
                }
        }
    }

    @IBAction func btnSave() {
        do {
            try
                symbol.writeToFile(filePath,
                    atomically: true,
                    encoding: NSUTF8StringEncoding)
                displayAlert("Saved", message: "String saved successfully")
        } catch let error as NSError {
             displayAlert("Error", message: "(error)")
        }
    }

    @IBAction func btnLoad()  {
        do {
            try
             symbol = String(contentsOfFile: filePath,
                 encoding: NSUTF8StringEncoding)
             displayAlert("Retrieved", message: "Symbol is " + symbol)
        } catch let error as NSError {
             displayAlert("Error", message: "(error)")
        }
    }

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)

        // Configure interface objects here.
        if let dir : NSString = NSSearchPathForDirectoriesInDomains(
            NSSearchPathDirectory.DocumentDirectory,
            NSSearchPathDomainMask.AllDomainsMask, true).first {
            filePath = dir.stringByAppendingPathComponent("myfile.txt");
        }
    }

In the awakeWithContext: method, you first created a path to a file named myfile.txt in the Documents directory. This file is used to save the contents of a string. You then used the presentTextInputControllerWithSuggestions:allowedInputMode: method to ask the user to input some text. When the text has been entered, it is displayed in the Label control. When the user clicks the Save button, you save the text using the writeToFile:atomically:encoding: method of the String structure. To load the text from the file, you use the initializer of the String structure.

2. Select the FileStorage WatchKit App scheme in Xcode and deploy the projects onto the iPhone and Apple Watch Simulators.

3. On the Apple Watch Simulator, click the Input Symbol button. Select a symbol (say, AAPL), and you should now see it displayed in the Label control (see Figure 7.30). Clicking the Save button saves the text into the watch. Clicking the Load button displays the contents of the saved file.

Image

Figure 7.30 Saving the string using a file

Using NSUserDefaults

The file method is usually the easiest and best way to save string content. However, for storing structured data (such as key/value pairs), you are better off using the NSUserDefaults class:

1. Add the following statements in bold to the InterfaceController.swift file:

    @IBAction func btnSave() {
        do {
            try
                symbol.writeToFile(filePath,
                    atomically: true,
                    encoding: NSUTF8StringEncoding)
                displayAlert("Saved", message: "String saved successfully")
        } catch let error as NSError {
             displayAlert("Error", message: "(error)")
        }

        let defaults = NSUserDefaults.standardUserDefaults()
        defaults.setValue(symbol, forKey: "symbol")
        defaults.synchronize()
    }

    @IBAction func btnLoad()  {
        do {
            try
             symbol = String(contentsOfFile: filePath,
                 encoding: NSUTF8StringEncoding)
             displayAlert("Retrieved", message: "Symbol is " + symbol)
        } catch let error as NSError {
             displayAlert("Error", message: "(error)")
        }

        let defaults = NSUserDefaults.standardUserDefaults()
        let str = defaults.stringForKey("symbol")
        print("(str)")
    }

2. Select the FileStorage WatchKit App scheme in Xcode and deploy the projects onto the iPhone and Apple Watch Simulators.

3. On the Apple Watch Simulator, click the Input Symbol button. Select a symbol (say, AAPL), and you should now see it displayed in the Label control. Clicking the Save button saves the text onto the watch. Clicking the Load button displays the contents of the saved string in the Output window.

Summary

In this chapter, you saw how your Apple Watch app can communicate with the containing iOS through the various modes supported: Application Context, User Info, File Transfer, and Interactive Messaging. Each mode has its intended usage, which should be sufficient to satisfy most of your needs. In addition, you also learned how the watch app can communicate with the outside world even if the iPhone is not available. Finally, you saw two easy ways to persist data on your Apple Watch.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.76.218