iOS 9 Swift Programming Cookbook (2015)
Chapter 2. Apple Watch
Version 2 of watchOS gives us developers a lot more control and brings cool featuers to the users as well. Now that we can download files directly and get access to sensors directly on the watch, the users will benefit.
In this chapter, I am going to assume that you have a simple iOS application in Xcode already created and you want to add a watchOS 2 target to your app. So go to Xcode and create a new Target. On the new window, on the left hand side, under the watchOS category, choose WatchKit App (see Figure 2-1) and proceed to the next stage.
Figure 2-1. Adding a WatchKit App target to our main application
In the next stage, make sure that you have enabled complications (we’ll talk about it later) and the glance scene (see Figure 2-2).
Figure 2-2. Add a complication and a glance scene to your watch app
After you have created your watch extension, you want to be able to run it on the simulator. To do this, simply choose your app from the targets in Xcode and press the run button (see Figure 2-3).
Figure 2-3. A simple watch interface
2.1 Downloading Files Onto the Apple Watch
Problem
You want to be able to download files from your watch app directly without needing to communicate your intentions to the paired iOS device.
Solution
Use NSURLSession as you would on a phone, but with more consideration towards resources and the size of the file you are downloading.
Always consider whether you need the file immediately not. If you need the file and the size is quite manageable, download it on the watch itself. If the file is big, try to download it on the companion app on the iOS device first and then send the file over to the watch, which itself takes some time.
Discussion
Let’s create an interface similar to Figure 2-4 in our watch extension.
Figure 2-4. Place a label and a button on your interface
Make sure the label can contain at least four lines of text (see Figure 2-5).
Figure 2-5. Lines property must be set to at least 4
Hook up your button’s action to a method in your code named download. Also hook up your label to code under the name statusLbl.
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController, NSURLSessionDelegate,
NSURLSessionDownloadDelegate {
@IBOutlet var statusLbl: WKInterfaceLabel!
var status: String = ""{
didSet{
dispatch_async(dispatch_get_main_queue()){[unowned self] in
self.statusLbl.setText(self.status)
}
}
}
...
NOTE
Since NSURLSession delegate methods get called on private queues (not the main thread), I’ve coded a property on our class called status. This is a string property that functions on the private thread can set to indicate what they’re doing, and that is displayed as the text on our label by the main thread.
The most important method of the NSURLSessionDownloadDelegate protocol that we are going to have to implement is the URLSession(_:downloadTask:didFinishDownloadingToURL:) method. It gets called when our file has been downloaded into a URL onto the disk, accessible to the watch. The file there is temporary: when this method returns, the file will be deleted by watchOS. In this method, you can do two things:
§ Read the file directly from the given URL. If you do so, you have to do the reading on a separate thread so that you won’t block NSURLSession’s private queue.
§ Move the file using NSFileManager to another location that is accessible to your extension and then read it later.
We are going to move this file to a location that will later be accessible to our app.
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didFinishDownloadingToURL location: NSURL) {
let fm = NSFileManager()
let url = try! fm.URLForDirectory(.DownloadsDirectory,
inDomain: .UserDomainMask,
appropriateForURL: location, create: true)
.URLByAppendingPathComponent("file.txt")
do{
try fm.removeItemAtURL(url)
try fm.moveItemAtURL(location, toURL: url)
self.status = "Download finished"
} catch let err{
self.status = "Error = \(err)"
}
session.invalidateAndCancel()
}
The task that we are going to start in order to download the file (you’ll see that soon) will have an identifier. This identifier is quite important for control the task after we have started it.
You can see that we also have to call the invalidateAndCancel() method on our task so that we can reuse the same task identifier later. If you don’t do this, the next time you tap on the button to re-download the item you won’t be able to.
We are then going to implement a few more useful methods from NSURLSessionDelegate and NSURLSessionDownloadDelegate just so we can show relevant status messages to the user as we are downloading the file:
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64,
totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
status = "Downloaded \(bytesWritten) bytes"
}
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
status = "Resuming the download"
}
func URLSession(session: NSURLSession, task: NSURLSessionTask,
didCompleteWithError error: NSError?) {
if let e = error{
status = "Completed with error = \(e)"
} else {
status = "Finished"
}
}
func URLSession(session: NSURLSession,
didBecomeInvalidWithError error: NSError?) {
if let e = error{
status = "Invalidated \(e)"
} else {
//no errors occurred, so that's alright
}
}
When the user taps the download button, we first define our URL:
let url = NSURL(string: "http://localhost:8888/file.txt")!
NOTE
I am running MAMP and hosting my own file called file.txt. This url won’t get downloaded successfully on your machine if you are not hosting the exact same file with the same name on your local machine on the same port! So I suggest that you change this URL to something that makes more sense for your app.
Then use the backgroundSessionConfigurationWithIdentifier(_:) class method of NSURLSessionConfiguration to create a background URL configuration that you can use with NSURLSession:
let id = "se.pixolity.app.backgroundtask"
let conf = NSURLSessionConfiguration
.backgroundSessionConfigurationWithIdentifier(id)
Once all of that is done, you can go ahead and create a download task and start it (see Figure 2-6).
let session = NSURLSession(configuration: conf, delegate: self,
delegateQueue: NSOperationQueue())
let request = NSURLRequest(URL: url)
session.downloadTaskWithRequest(request).resume()
Figure 2-6. Our file is successfully downloaded
See Also