iOS 9 Swift Programming Cookbook (2015)
Chapter 2. Apple Watch
2.3 Transferring Small Pieces of Data To and From the Watch
Problem
You want to transfer some plist-serializable content between your apps (iOS and watchOS). This content can be anything: for instance, information about where a user is inside a game on an iOS device, or more random information that you can serialize into a plist (strings, integers, booleans, dictionaries and arrays). Information can be sent it either direction.
Solution
Follow these steps:
1. Use what you learned in Recipe 2.2 to find out whether both devices are reachable.
2. On the sending app, use the updateApplicationContext(_:) method of your session to send the content over to the other app.
3. On the receiving app, wait for the session(_:didReceiveApplicationContext:) delegate method of WCSessionDelegate, where you will be given access to the transmitted content.
NOTE
The content that you transmit must be of type [String : AnyObject].
Discussion
Various types of content can be sent between iOS and watchOS. One is plist-serializable content, also be called an application context. Let’s say that you are playing a game on watchOS and you want to send the user’s game status to iOS. You can use the application context for this.
Let’s begin by creating a sample application. Create a single view iOS app and add a watchOS target to it as well (see Figure 2-1). Design your main interface like Figure 2-7. We’ll use the top label to show the download status. The buttons are self explanatory. The bottom label will show the pairing status between our watchOS and iOS apps.
Figure 2-7. Labels and button for sample app
NOTE
Hook up the top label to your view controller as statusLbl, the first button as sendBtn, the second button as downloadBtn, and the bottom label as reachabilityStatusLbl. Hook up the action of the download button to a method called download() and the send button to a method called send().
Download and install MAMP (it’s free) and host the following contents as a file called people.json on your local web server’s root folder:
{
"people" : [
{
"name" : "Foo",
"age" : 30
},
{
"name" : "Bar",
"age" : 50
}
]
}
Now the top part of your iOS app’s view controller should look like this:
import UIKit
import WatchConnectivity
class ViewController: UIViewController, WCSessionDelegate,
NSURLSessionDownloadDelegate {
@IBOutlet var statusLbl: UILabel!
@IBOutlet var sendBtn: UIButton!
@IBOutlet var downloadBtn: UIButton!
@IBOutlet var reachabilityStatusLbl: UILabel!
...
When you download that JSON file, it will become a dictionary of type [String : AnyObject], so let’s define that as a variable in our vc:
var people: [String : AnyObject]?{
didSet{
dispatch_async(dispatch_get_main_queue()){
self.updateSendButton()
}
}
}
func updateSendButton(){
sendBtn.enabled = isReachable && isDownloadFinished && people != nil
}
NOTE
Setting the value of the people variable will call the updateSendButton() function, which in turn enables the send button only if all the following conditions are met:
1. The watch app is reachable.
2. The file is downloaded.
3. The file was correctly parsed into the people variable.
Also define a variable that can write into your status label whenever the reachability flag is changed:
var isReachable = false{
didSet{
dispatch_async(dispatch_get_main_queue()){
self.updateSendButton()
if self.isReachable{
self.reachabilityStatusLbl.text = "Watch is reachable"
} else {
self.reachabilityStatusLbl.text = "Watch is not reachable"
}
}
}
}
We need two more properties: one that sets the status label and another that keeps track of when our file is downloaded successfully:
var isDownloadFinished = false{
didSet{
dispatch_async(dispatch_get_main_queue()){
self.updateSendButton()
}
}
}
var status: String?{
get{return self.statusLbl.text}
set{
dispatch_async(dispatch_get_main_queue()){
self.statusLbl.text = newValue
}
}
}
NOTE
All these three variables (people, isReachable, and isDownloadFinished) that we defined call the updateSendButton() function so that our send button will be disabled if conditions are not met, and enabled otherwise.
Now when the download button is pressed, start a download task:
@IBAction func download() {
//if loading HTTP content, make sure you have disabled ATS
//for that domain
let url = NSURL(string: "http://localhost:8888/people.json")!
let req = NSURLRequest(URL: url)
let id = "se.pixolity.app.backgroundtask"
let conf = NSURLSessionConfiguration
.backgroundSessionConfigurationWithIdentifier(id)
let sess = NSURLSession(configuration: conf, delegate: self,
delegateQueue: NSOperationQueue())
sess.downloadTaskWithRequest(req).resume()
}
After that, check ifyoue got any errors while trying to download the file:
func URLSession(session: NSURLSession, task: NSURLSessionTask,
didCompleteWithError error: NSError?) {
if error != nil{
status = "Error happened"
isDownloadFinished = false
}
session.finishTasksAndInvalidate()
}
Now implement the URLSession(_:downloadTask:didFinishDownloadingToURL:) method of NSURLSessionDownloadDelegate. Inside there, tell your view controller that you have downloaded the file by setting isDownloadFinished to true. Then construct a more permanent URL for the temporary URL to which our JSON file was downloaded by iOS:
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didFinishDownloadingToURL location: NSURL){
isDownloadFinished = true
//got the data, parse as JSON
let fm = NSFileManager()
let url = try! fm.URLForDirectory(.DownloadsDirectory,
inDomain: .UserDomainMask,
appropriateForURL: location,
create: true).URLByAppendingPathComponent("file.json")
...
Then move the file over:
do {try fm.removeItemAtURL(url)} catch {}
do{
try fm.moveItemAtURL(location, toURL: url)
} catch {
status = "Could not save the file"
return
}
After that, simply read the file as a JSON file with NSJSONSerialization:
//now read the file from url
guard let data = NSData(contentsOfURL: url) else{
status = "Could not read the file"
return
}
do{
let json = try NSJSONSerialization.JSONObjectWithData(data,
options: .AllowFragments) as! [String : AnyObject]
self.people = json
status = "Successfully downloaded and parsed the file"
} catch{
status = "Could not read the file as json"
}
Great--now go to your watch interface, place a label there, and hook it up to your code under the name statusLabel (see Figure 2-8).
Figure 2-8. Our watch interface has a simple label only
In the interface controller file, place a variable that can set the status:
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
@IBOutlet var statusLabel: WKInterfaceLabel!
var status = "Waiting"{
didSet{
statusLabel.setText(status)
}
}
}
Go to your ExtensionDelegate file on the watch side and do these things:
1. Define a structure that can hold instances of a person you will get in our application context.
2. Define a property called status that when set, will set the status property of the interface controller.
import WatchKit
import WatchConnectivity
struct Person{
let name: String
let age: Int
}
class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate{
var status = ""{
didSet{
dispatch_async(dispatch_get_main_queue()){
guard let interface =
WKExtension.sharedExtension().rootInterfaceController as?
InterfaceController else{
return
}
interface.status = self.status
}
}
}
...
Now activate the session using what you learned in Recipe 2.2. I won’t write the code for that in this recipe again. Then the session will wait for the session(_:didReceiveApplicationContext:) method of the WCSessionDelegate protocol to come in. When that happens, just read the application context and convert it into Person instances.
func session(session: WCSession,
didReceiveApplicationContext applicationContext: [String : AnyObject]) {
guard let people = applicationContext["people"] as?
Array<[String : AnyObject]> where people.count > 0 else{
status = "Did not find the people array"
return
}
var persons = [Person]()
for p in people where p["name"] is String && p["age"] is Int{
let person = Person(name: p["name"] as! String, age: p["age"] as! Int)
persons.append(person)
}
status = "Received \(persons.count) people from the iOS app"
}
Now run both your watch app and your iOS app. At first glance, your watch app will look like Figure 2-9.
Figure 2-9. Our watch app is waiting for the context to come through from the iOS app
Our iOS app in its initial state will look like Figure 2-10).
Figure 2-10. Our iOS app has detected that its companion watch app is reachable
When I press the download button, my iOS app’s interface will change to Figure 2-11.
Figure 2-11. The iOS app is now ready to send the data over to the watch app
After pressing the send button, the watch app’s interface will change to something like Figure 2-12.
Figure 2-12. The watch app received the data
See Also