Documents and iCloud - Beginning iPhone Development with Swift: Exploring the iOS SDK (2014)

Beginning iPhone Development with Swift: Exploring the iOS SDK (2014)

Chapter 14. Documents and iCloud

One of the biggest new features added to iOS in the past couple of years is Apple’s iCloud service, which provides cloud storage services for iOS devices, as well as for computers running OS X. Most iOS users will probably encounter the iCloud device backup option immediately when setting up a new device or upgrading an old device to a more recent version of iOS. And they will quickly discover the advantages of automatic backup that doesn’t even require the use of a computer.

Computerless backup is a great feature, but it only scratches the surface of what iCloud can do. What may be even a bigger feature of iCloud is that it provides app developers with a mechanism for transparently saving data to Apple’s cloud servers with very little effort. You can make your apps save data to iCloud and have that data automatically transfer to any other devices that are registered to the same iCloud user. Users may create a document on their iPad and later view the same document on their iPhone or Mac without any intervening steps; the document just appears.

A system process takes care of making sure the user has a valid iCloud login and manages the file transfers, so you don’t need to worry about networks or authentication. Apart from a small amount of app configuration, just a few small changes to your methods for saving files and locating available files will get you well on your way to having an iCloud–backed app.

One key component of the iCloud filing system is the UIDocument class. UIDocument takes a portion of the work out of creating a document-based app by handling some of the common aspects of reading and writing files. That way, you can spend more of your time focusing on the unique features of your app, instead of building the same plumbing for every app you create.

Whether you’re using iCloud or not, UIDocument provides some powerful tools for managing document files in iOS. To demonstrate these features, the first portion of this chapter is dedicated to creating TinyPix, a simple document-based app that saves files to local storage. This is an approach that can work well for all kinds of iOS-based apps.

Later in this chapter, we’ll show you how to iCloud-enable TinyPix. For that to work, you’ll need to have one or more iCloud-connected iOS devices at hand. You’ll also need a paid iOS developer account, so that you can install on devices. This is because apps running in the simulator don’t have access to iCloud services.

Managing Document Storage with UIDocument

Anyone who has used a desktop computer for anything besides just surfing the Web has probably worked with a document-based application. From TextEdit to Microsoft Word to GarageBand to Xcode, any piece of software that lets you deal with multiple collections of data, saving each collection to a separate file, could be considered a document-based application. Often, there’s a one-to-one correspondence between an on-screen window and the document it contains; however, sometimes (e.g., Xcode) a single window can display multiple documents that are all related in some way.

On iOS devices, we don’t have the luxury of multiple windows, but plenty of apps can still benefit from a document-based approach. Now iOS developers have a little boost in making it work—thanks to the UIDocument class, which takes care of the most common aspects of document file storage. You won’t need to deal with files directly (just URLs), and all the necessary reading and writing happens on a background thread, so your app can remain responsive even while file access is occurring. It also automatically saves edited documents periodically and whenever the app is suspended (such as when the device is shut down, the Home button is pressed, and so on), so there’s no need for any sort of save button. All of this helps make your apps behave the way users expect their iOS apps to behave.

Building TinyPix

We’re going to build an app called TinyPix that lets you edit simple 8 × 8 images, in glorious 1-bit color (see Figure 14-1)! For the user’s convenience, each picture is blown up to the full screen size for editing. And, of course, we’ll be using UIDocument to represent the data for each image.

image

Figure 14-1. Editing an extremely low-resolution icon in TinyPix

Start off by creating a new project in Xcode. From the iOS Application section, select the Master-Detail Application template and click Next. Name this new app TinyPix and set the Devices pop-up to Universal. Make sure the Use Core Data check box is unchecked. Now click Next again and choose the location to save your project.

In Xcode’s Project Navigator, you’ll see that your project contains files for AppDelegate, MasterViewController, and DetailViewController, as well as the Main.storyboard file. We’ll make changes to all of these files and we will create a few new classes along the way, as well.

Creating TinyPixDocument

The first new class we’re going to create is the document class that will contain the data for each TinyPix image that’s loaded from file storage. Select the TinyPix folder in Xcode and press imageN to create a new file. From the iOS section, select Cocoa Touch Class and click Next. EnterTinyPixDocument in the Class field, enter UIDocument in the Subclass of field, and click Next. Finally, click Create to create the file.

Let’s think about the public API of this class before we get into its implementation details. This class is going to represent an 8 × 8 grid of pixels, where each pixel consists of a single on or off value. So, we’ll give it a method that takes a pair of row and column indexes and returns a Boolvalue. We’ll also provide a method to set a specific state at a specified row and column and, as a convenience, another method that simply toggles the state at a particular place.

Switch over to TinyPixDocument.swift, where we’ll implement storage for our 8 × 8 grid, the methods that we need for our public API, and the required UIDocument methods that will enable loading and saving our documents.

Let’s start by defining the storage for our 8 × 8 bitmap data. We’ll hold this data in an array of UInt8. Add the following property to the TinyPixDocument class:

class TinyPixDocument: UIDocument {
private var bitmap: [Byte]

The UIDocument class has a designated initializer that all subclasses should use. This is where we’ll create our initial bitmap. In true bitmap style, we’re going to minimize memory usage by using a single byte to contain each row. Each bit in the byte represents the on/off value of a column index within that row. In total, our document contains just 8 bytes.

Note This section contains a small number of bitwise operations, as well as some C pointer and array manipulation. This is all pretty mundane for C developers; but if you don’t have much C experience, it may seem puzzling or even impenetrable. In that case, feel free to simply copy and use the code provided (it works just fine). If you really want to understand what’s going on, you may want to dig deeper into C itself, perhaps by adding a copy of Learn C on the Mac by Dave Mark (Apress, 2009) to your bookshelf.

Add this method to our document’s implementation:

override init(fileURL: NSURL) {
bitmap = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]
super.init(fileURL: fileURL)
}

This starts off the bitmap with a simple diagonal pattern stretching from one corner to another.

Now, it’s time to implement the methods that make up the public API. Let’s first create a method that reads the state of a single bit from the bitmap. This simply grabs the relevant byte from our array of bytes, and then does a bit shift and an AND operation to determine whether the specified bit was set, returning true or false accordingly. Add this method:

func stateAt(#row: Int, column: Int) -> Bool {
let rowByte = bitmap[row]
let result = Byte(1 << column) & rowByte
return result != 0
}

Next comes the inverse: a method that sets the value specified at a given row and column. Here, we once again grab the byte for the specified row and do a bit shift. But this time, instead of using the shifted bit to examine the contents of the row, we use it to either set or unset a bit in the row. Add this method at the end of the class definition:

func setState(state: Bool, atRow row: Int, column: Int) {
var rowByte = bitmap[row]
if state {
rowByte |= Byte(1 << column)
} else {
rowByte &= ~Byte(1 << column)
}
bitmap[row] = rowByte
}

Now, let’s add a convenience method that lets outside code simply toggle a single cell:

func toggleStateAt(#row: Int, column: Int) {
let state = stateAt(row: row, column: column)
setState(!state, atRow: row, column: column)
}

Our document class requires two final pieces before it fits into the puzzle of a document-based app: methods for reading and writing. As we mentioned earlier, you don’t need to deal with files directly. You don’t even need to worry about the URL that was passed into the init(fileURL:)initializer earlier. All that you need to do is implement one method that transforms the document’s data structure into an NSData object, ready for saving, and another that takes a freshly loaded NSData object and pulls the object’s data structure out of it. Add these two methods that implement the required UIDocument contract:

override func contentsForType(typeName: String, error outError: NSErrorPointer) -> AnyObject? {
println("Saving document to URL \(fileURL)")
let bitmapData = NSData(bytes: bitmap, length: bitmap.count)
return bitmapData
}

override func loadFromContents(contents: AnyObject, ofType typeName: String,
error outError: NSErrorPointer) -> Bool {
println("Loading document from URL \(fileURL)")
let bitmapData = contents as NSData
bitmapData.getBytes(UnsafeMutablePointer<Byte>(bitmap), length: bitmap.count)
return true
}

The first of these methods, contentsForType(_, error:), is called whenever our document is about to be saved to storage. It simply returns a copy of our bitmap wrapped in an NSData object, which the system will take care of storing later.

The second method, loadFromContents(_, ofType:, error:), is called whenever the system has just loaded data from storage and wants to provide this data to an instance of our document class. Here, we just grab a copy of the bytes from the NSData object that has been passed in. We’ve included some logging statements, just so you can see what’s happening in the Xcode log later on.

Each of these methods allows you to do some things that we’re ignoring in this app. They both provide a typeName parameter, which you could use to distinguish between different types of data storage that your document can load from or save to. They also have an outError parameter, which you could use to specify that an error occurred while copying data to or from your document’s in-memory data structure. In our case, however, what we’re doing is so simple that these aren’t important concerns.

That’s all we need for our document class. Sticking to MVC principles, our document sits squarely in the model camp, knowing nothing about how it’s displayed. And thanks to the UIDocument superclass, the document is even shielded from most of the details about how it’s stored.

Code Master

Now that we have our document class ready to go, it’s time to address the first view that a user sees when running our app: the list of existing TinyPix documents, which is taken care of by the MasterViewController class. We need to let this class know how to grab the list of available documents, let the user choose an existing document for viewing or editing, and create and name a new document. When a document is created or chosen, it’s then passed along to the detail controller for display.

Start by selecting MasterViewController.swift. This file, generated as part of the Master–Detail application template, contains starter code for displaying an array of items. We’re not going to use any of that, but instead do these things all on our own. Therefore, delete everything in the file apart from the import of the UIKit framework and the class declaration. When you’re done, you should have a clean slate that looks like this:

import UIKit

class MasterViewController: UITableViewController {
}

We’ll also include a segmented control in our GUI, which will allow the user to choose a tint color that will be used as a highlight color for portions of the TinyPix GUI. Although this is not a particularly useful feature in and of itself, it will help demonstrate the iCloud mechanism, as the highlight color setting makes its way from the device on which you set it to another of your connected devices running the same app. The first version of the app will use the color as a local setting on each device. Later in the chapter, we’ll add the code to make the color setting propagate through iCloud to the user’s other devices.

To implement the color selection control, we’ll add an outlet and an action to our code as well. We’ll also add properties for holding onto a list of document file names and a pointer to the document the user has chosen. Make these changes to MasterViewController.swift:

class MasterViewController: UITableViewController {
@IBOutlet var colorControl: UISegmentedControl!
private var documentFileNames: [String] = []
private var chosenDocument: TinyPixDocument?

Before we implement the table view methods and other standard methods that we need to deal with, we are going to write a couple of private utility methods. The first of these takes a file name, combines it with the file path of the app’s Documents directory, and returns a URL pointing to that specific file. As you saw in Chapter 13, the Documents directory is a special location that iOS sets aside, one for each app installed on an iOS device. You can use it to store documents created by your app, and rest assured that those documents will be automatically included whenever users back up their iOS device, whether it’s to iTunes or iCloud.

Add this method to MasterViewController.swift:

private func urlForFileName(fileName: NSString) -> NSURL {
let fm = NSFileManager.defaultManager()
let urls = fm.URLsForDirectory(NSSearchPathDirectory.DocumentDirectory,
inDomains: NSSearchPathDomainMask.UserDomainMask) as [NSURL]
let directoryURL = urls[0]
let fileURL = directoryURL.URLByAppendingPathComponent(fileName)
return fileURL
}

Here, we are using a method of the NSFileManager class to get a URL that maps to the application’s Documents directory. This method works just like the NSSearchPathForDirectoriesInDomains() function that we used in Chapter 13, except that it returns an array of NSURLobjects instead of strings, which is more convenient for the purposes of this method.

The second private method is a bit longer. It also uses the Documents directory, this time to search for files representing existing documents. The method takes the files it finds and sorts them by creation date, so that the user will see the list of documents sorted “blog-style” with the newest items first. The document file names are stashed away in the documentFilenames property, and then the table view (which we admittedly haven’t yet dealt with) is reloaded. Add this code to the class definition:

private func reloadFiles() {
let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,
NSSearchPathDomainMask.UserDomainMask, true) as [String]
let path = paths[0]
let fm = NSFileManager.defaultManager()

var error:NSError? = nil;
let files = fm.contentsOfDirectoryAtPath(path, error: &error) as? [String]
if files != nil {
let sortedFileNames = sorted(files!) { fileName1, fileName2 in
let file1Path = path.stringByAppendingPathComponent(fileName1)
let file2Path = path.stringByAppendingPathComponent(fileName2)
let attr1 = fm.attributesOfItemAtPath(file1Path, error: nil)
let attr2 = fm.attributesOfItemAtPath(file2Path, error: nil)
let file1Date = attr1![NSFileCreationDate] as NSDate
let file2Date = attr2![NSFileCreationDate] as NSDate
let result = file1Date.compare(file2Date)
return result == NSComparisonResult.OrderedAscending
}

documentFileNames = sortedFileNames
tableView.reloadData()
} else {
println("Error listing files in directory \(path): \(error)")
}
}

Now, let’s deal with our dear old friends, the table view data source methods. These should be pretty familiar to you by now. Add the following three methods to MasterViewController.swift:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return documentFileNames.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath
indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("FileCell") as UITableViewCell
let path = documentFileNames[indexPath.row]
cell.textLabel.text = path.lastPathComponent.stringByDeletingPathExtension
return cell
}

These methods are based on the contents of the array stored in the documentFilenames property. The tableView(_, cellForForAtIndexPath:) method relies on the existence of a cell attached to the table view with "FileCell" set as its identifier, so we must be sure to set that up in the storyboard a little later.

If not for the fact that we haven’t touched our storyboard yet, the code we have now would almost be something we could run and see in action; however, with no preexisting TinyPix documents, we would have nothing to display in our table view. And so far, we don’t have any way to create new documents, either. Also, we have not yet dealt with the color-selection control we’re going to add. So, let’s do a bit more work before we try to run our app.

The user’s choice of highlight color will be used to immediately set a tint color for the segmented control. The UIView class has a tintColor property. When it’s set for any view, the value applies to that view and will propagate down to all of its subviews. When we set the segmented control’s tint color, we’ll also store it in NSUserDefaults for later retrieval. Add these two methods to the end of the class definition:

@IBAction func chooseColor(sender: UISegmentedControl) {
let selectedColorIndex = sender.selectedSegmentIndex
setTintColorForIndex(selectedColorIndex)

let prefs = NSUserDefaults.standardUserDefaults()
prefs.setInteger(selectedColorIndex, forKey: "selectedColorIndex")
prefs.synchronize()
}

private func setTintColorForIndex(colorIndex: Int) {
colorControl.tintColor = TinyPixUtils.getTintColorForIndex(colorIndex)
}

The first method is triggered when the user changes the selection in the segmented control. It saves the selected index in the user defaults and passes it to the second method, which converts the index to a color and applies it to the segmented control. We’ll need the code that does the conversion from index to color in the detail view controller as well, so it’s implemented in a separate class. To create that class, press imageN to open the new file dialog. From the iOS section, select Swift File and click Next. Enter TinyPixUtils.swift as the file name and click Create to create the file.

Now switch over to TinyPixUtils.swift to implement the method that we need:

import Foundation
import UIKIt

class TinyPixUtils {
class func getTintColorForIndex(index: Int) -> UIColor {
var color: UIColor
switch index {
case 0:
color = UIColor .redColor()

case 1:
color = UIColor(red: 0, green: 0.6, blue: 0, alpha: 1)

case 2:
color = UIColor.blueColor()

default:
color = UIColor.redColor()
}
return color
}
}

We realize that we haven’t yet set anything up in the storyboard, but we’ll get there! First, we have some more work to do in MasterViewController.swift. Let’s start with the viewDidLoad method. After calling the superclass’s implementation, we’ll add a button to the right side of the navigation bar. The user will press this button to create a new TinyPix document. We’ll also load the saved tint color from the user defaults and use it to set the tint color of the segmented control. We finish by calling the reloadFiles() method that we implemented earlier.

Add this code to implement viewDidLoad():

override func viewDidLoad() {
super.viewDidLoad()

let addButton = UIBarButtonItem(
barButtonSystemItem: UIBarButtonSystemItem.Add,
target: self, action: "insertNewObject")
navigationItem.rightBarButtonItem = addButton

let prefs = NSUserDefaults.standardUserDefaults()
let selectedColorIndex = prefs.integerForKey("selectedColorIndex")
setTintColorForIndex(selectedColorIndex)
colorControl.selectedSegmentIndex = selectedColorIndex

reloadFiles()
}

As you’ll see when you run the app for the first time, the segmented control’s tint color starts out being red. That’s because there’s nothing stored in the user defaults yet, so the integerForKey() method returns 0, which the setTintColorForIndex() method interprets as red.

You may have noticed that, when we created the UIBarButtonItem, we told it to call the insertNewObject() method when it’s pressed. We haven’t written that method yet, so let’s do so now. Add this method definition:

func insertNewObject() {
let alert = UIAlertController(title: "Choose File Name",
message: "Enter a name for your new TinyPix document",
preferredStyle: .Alert)
alert.addTextFieldWithConfigurationHandler(nil)

let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
let createAction = UIAlertAction(title: "Create", style: .Default) { action in
let textField = alert.textFields![0] as UITextField
self.createFileNamed(textField.text)
};

alert.addAction(cancelAction)
alert.addAction(createAction)

presentViewController(alert, animated: true, completion: nil)
}

This method uses the UIAlertController class to display an alert that includes a text-input field, a Create button, and a Cancel button. If the Create button is pressed, the responsibility of creating a new item instead falls to the method that the button’s handler block calls when it’s finished, which we’ll also add now. Add this method:

private func createFileNamed(fileName: String) {
let trimmedFileName = fileName.stringByTrimmingCharactersInSet(
NSCharacterSet.whitespaceCharacterSet())
if !trimmedFileName.isEmpty {
let targetName = trimmedFileName + ".tinypix"
let saveUrl = urlForFileName(targetName)
chosenDocument = TinyPixDocument(fileURL: saveUrl)
chosenDocument?.saveToURL(saveUrl,
forSaveOperation: UIDocumentSaveOperation.ForCreating,
completionHandler: { success in
if success {
println("Save OK")
self.reloadFiles()
self.performSegueWithIdentifier("masterToDetail", sender: self)
} else {
println("Failed to save!")
}
})
}
}

This method starts out simply enough. It strips leading and trailing whitespace characters from the name that it’s passed. If the result is not empty, it then creates a file name based on the user’s entry, a URL based on that file name (using the urlForFilename() method we wrote earlier), and a new TinyPixDocument instance using that URL.

What comes next is a little more subtle. It’s important to understand here that just creating a new document with a given URL doesn’t create the file. In fact, at the time that init(fileURL:) is called, the document doesn’t yet know if the given URL refers to an existing file or to a new file that needs to be created. We need to tell it what to do. In this case, we tell it to save a new file at the given URL with this code:

chosenDocument?.saveToURL(saveUrl,
forSaveOperation: UIDocumentSaveOperation.ForCreating,
completionHandler: { success in
.
.
.
})

Of interest is the purpose and usage of the closure that is passed in as the last argument. The method we’re calling, saveToURL(_, forSaveOperation:, completionHandler:), doesn’t have a return value to tell us how it all worked out. In fact, the method returns immediately after it’s called, long before the file is actually saved. Instead, it starts the file-saving work and later, when it’s done, calls the closure that we gave it, using the success parameter to let us know whether it succeeded. To make it all work as smoothly as possible, the file-saving work is actually performed on a background thread. The closure we pass in, however, is executed on the thread that called saveToURL(_, forSaveOperation:, completionHandler:) in the first place. In this particular case, that means that the block is executed on the main thread, so we can safely use any facilities that require the main thread, such as UIKit. With that in mind, take a look again at what happens inside that block:

if success {
println("Save OK")
self.reloadFiles()
self.performSegueWithIdentifier("masterToDetail", sender: self)
} else {
println("Failed to save!")
}

This is the content of the block we passed in to the file-saving method, and it’s called later, after the file operation is completed. We check to see if it succeeded; if so, we do an immediate file reload, and then initiate a segue to another view controller. This is an aspect of segues that we didn’t cover in Chapter 9, but it’s pretty straightforward.

The idea is that a segue in a storyboard file can have an identifier, just like a table view cell, and you can use that identifier to trigger a segue programmatically. In this case, we’ll just need to remember to configure that segue in the storyboard when we get to it. But before we do that, let’s add the last method this class needs, to take care of that segue. Add this method to MasterViewController.swift:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let destination =
segue.destinationViewController as UINavigationController
let detailVC =
destination.topViewController as DetailViewController

if sender === self {
// if sender === self, a new document has just been created,
// and chosenDocument is already set.
detailVC.detailItem = chosenDocument
} else {
// Find the chosen document from the tableview
let indexPath = tableView.indexPathForSelectedRow()!
let filename = documentFileNames[indexPath.row]
let docURL = urlForFileName(filename)
chosenDocument = TinyPixDocument(fileURL: docURL)
chosenDocument?.openWithCompletionHandler() { success in
if success {
println("Load OK")
detailVC.detailItem = self.chosenDocument
} else {
println("Failed to load!")
}
}
}
}

This method has two clear paths of execution that are determined by the condition at the top. Remember from our discussion of storyboards in Chapter 9 that this method is called on a view controller whenever a segue is about to performed from that view controller. The sender parameter refers to the object that initiated the segue, and we use that to figure out just what to do here. If the segue is initiated by the programmatic method call we performed in the alert view delegate method, then sender will be equal to self, because that’s the value of the sender argument in the performSegueWithIdentifier(_, sender:) call in the createFileNamed() method. In that case, we know that the chosenDocument property is already set, and we simply pass its value to the destination view controller.

Otherwise, we know we’re responding to the user touching a row in the table view, and that’s where things get a little more complicated. That’s the time to construct a URL (much as we did when creating a document), create a new instance of our document class, and try to open the file. You’ll see that the method we call to open the file, openWithCompletionHandler(), works similarly to the save method we used earlier. We pass it a closure that it will save for later execution. Just as with the file-saving method, the loading occurs in the background, and this closure will be executed on the main thread when it’s complete. At that point, if the loading succeeded, we pass the document along to the detail view controller.

Note that both of these methods use the key-value coding technique that we’ve used a few times before, letting us set the detailItem property of the segue’s destination controller, even though we don’t include its header. This will work out just fine for us, sinceDetailViewController—the detail view controller class created as part of the Xcode project—happens to include a property called detailItem right out of the box.

With the amount of code we now have in place, it’s high time we configured the storyboard so that we can run our app and make something happen. Save your code and continue.

Initial Storyboarding

Select Main.storyboard in the Xcode Project Navigator and take a look at what’s already there. You’ll find scenes for a split view controller, two navigation controllers, the master view controller, and the detail view controller (see Figure 14-2). All of our work will be with the master and detail view controllers.

image

Figure 14-2. The TinyPix storyboard, showing split view controller, navigation controllers, master view controller, and detail view controller

Let’s start by dealing with the master view controller scene. This is where the table view showing the list of all our TinyPix documents is configured. By default, this scene’s table view is configured to use dynamic cells instead of static cells. We want our table view to get its contents from the data source methods we implemented, so this default setting is just what we want. We do need to configure the cell prototype though, so select it, and open the Attributes Inspector. Change the cell’s Identifier from Cell to FileCell. This will let the data source code we wrote earlier access the table view cell.

We also need to create the segue that we’re triggering in our code. Do this by Control-dragging from the master view controller’s icon (a yellow circle at the top of its scene or the Master icon under Master Scene in the Document Outline) over to the Navigation Controller for the detail view, and then selecting Show Detail from the storyboard segues menu.

You’ll now see two segues that seem to connect the two scenes. By selecting each of them, you can tell where they’re coming from. Selecting one segue highlights the whole master scene; selecting the second one highlights just the table view cell. Select the segue that highlights the whole scene (i.e., the segue that you just created), and use the Attributes Inspector to set its Identifier, which is currently empty, to masterToDetail.

The final touch needed for the master view controller scene is to let the user pick which color will be used to represent an “on” point in the detail view. Instead of implementing some kind of comprehensive color picker, we’re just going to add a segmented control that will let the user pick from a set of predefined colors.

Find a Segmented Control in the object library, drag it out, and place it in the navigation bar at the top of the master view (see Figure 14-3).

image

Figure 14-3. The TinyPix storyboard, showing the master view controller with a segmented control being dropped on the controller’s navigation bar

Make sure the segmented control is selected and then open the Attributes Inspector. In the Segmented Control section at the top of the inspector, use the stepper control to change the number of Segments from 2 to 3. Next, double-click the title of each segment in turn, changing them to Red,Green, and Blue, respectively. After setting those titles, click one of the resizing handles for the segmented control to make it fill out to the right width.

Next, Control-drag from the segmented control to the icon representing the master controller (the yellow circle labeled Master above the controller in the storyboard, or the Document Outline icon labeled Master under Master Scene) and select the chooseColor() method. Then Control-drag from the master controller back to the segmented control, and select the colorControl outlet.

We’ve finally reached a point where we can run the app and see all our hard work brought to life! Run your app. You’ll see it start up and display an empty table view with a segmented control at the top and a plus (+) button in the upper-right corner (see Figure 14-4).

image

Figure 14-4. The TinyPix app when it first appears. Click the plus icon to add a new document. You’ll be prompted to name your new TinyPix document. At the moment, all the detail view does is display the document name in a label

Hit the + button, and the app will ask you to name the new document. Give it a name, tap Create, and you’ll see the app transition to the detail display, which is, well, under construction right now. All the default implementation of the detail view controller does is display a (not very useful) description of its detailItem in a label. Of course, there’s more information in the console view in Xcode. It’s not much, but it’s something!

Tap the Back button to return to the master list, where you’ll see the item you added. Go ahead and create one or two more items to see that they’re correctly added to the list. Finally, head back to Xcode because we’ve got more work to do!

Creating TinyPixView

Our next order of business is the creation of a view class to display our grid and let the user edit it. Select the TinyPix folder in the Project Navigator, and press imageN to create a new file. In the iOS Source section, select Cocoa Touch Class and click Next. Name the new class TinyPixView and choose UIView in the Subclass of pop-up. Click Next, verify that the save location is OK, and then click Create.

Note The implementation of our view class includes some drawing and touch handling that we haven’t covered yet. Rather than bog down this chapter with too many details about these topics, we’re just going to quickly show you the code. We’ll cover details about drawing with Core Graphics in Chapter 16, and responding to touches and drags in Chapter 18.

Select TinyPixView.swift and add the following structure definition at the top of the file, before the class definition:

struct GridIndex {
var row: Int
var column: Int
}

We’ll use this structure whenever we need to refer to a (row, column) pair in the grid. Now add the following property definitions, which we’ll make use of shortly, to the class:

class TinyPixView: UIView {
var document: TinyPixDocument!
var lastSize: CGSize = CGSizeZero
var gridRect: CGRect!
var blockSize: CGSize!
var gap: CGFloat = 0
var selectedBlockIndex: GridIndex = GridIndex(row: NSNotFound, column: NSNotFound)

A UIView subclass is usually initialized by calling its init(frame:) method, which is its default initializer. However, since this class is going to be loaded from a storyboard, it will instead be initialized using the init(coder:) method. We’ll implement both of these initializers, making each call a third method that initializes our properties. Add the following code to TinyPixView.swift:

override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}

required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}

private func commonInit() {
calculateGridForSize(bounds.size)
}

The calculateGridForSize() method figures out how large the cells in the color grid should be, based on the size of TinyPixView. Calculating the grid size allows us to use the same application with screens of different sizes and also handles the case where the size of the view changes when the device is rotated. Add the implementation of the calculateGridForSize() method to TinyPixView.swift:

private func calculateGridForSize(size: CGSize) {
let space = min(size.width, size.height)
gap = space/57
let cellSide = gap * 6
blockSize = CGSizeMake(cellSide, cellSide)
gridRect = CGRectMake((size.width - space)/2, (size.height - space)/2,
space, space)
}

The idea behind this method is to make the grid fill either the full width or the full height of the view, whichever is the smaller, and to center it along the longer axis. To do that, we calculate the size of each cell, plus the gaps between the cells, by dividing the smaller dimension of the view by 57. Why 57? Well, we want to have space for eight cells and we want each cell to be six times the size of the intercell gap. Given that we need gaps between each pair of cell, plus a gap at the start and end of each row or column, that effectively means we need space for (6 × 8) + 9 = 57 gaps. Once we have the gap size, we get the size of each cell (by multiplying by 6). We use that information to set the value of the blockSize property, which represents the size of each cell, and the gridRect property, which corresponds to the region within the view in which the grid cells will actually be drawn.

Now let’s take a look at the drawing routines. We override the standard UIView drawRect() method, use that to simply walk through all the blocks in our grid, and then call another method that will draw each cell block. Add the following bold code and don’t forget to remove the comment marks around the drawRect() method:

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect)
{
if (document != nil) {
let size = bounds.size
if !CGSizeEqualToSize(size, lastSize) {
lastSize = size
calculateGridForSize(size)
}

for var row = 0; row < 8; row++ {
for var column = 0; column < 8; column++ {
drawBlockAt(row: row, column: column)
}
}
}
}
*/

Before we draw the cells, we compare the current size of the view to the value in the lastSize property and, if it’s different, we call calculateGridForSize(). This will happen when the view is first drawn and any time it changes size, which will most likely be when the device is rotated.

Now add the code that draws the block for each cell in the grid:

private func drawBlockAt(#row: Int, column: Int) {
let startX = gridRect.origin.x + gap
+ (blockSize.width + gap) * (7 - CGFloat(column)) + 1
let startY = gridRect.origin.y + gap
+ (blockSize.height + gap) * CGFloat(row) + 1

let blockFrame = CGRectMake(startX, startY,
blockSize.width, blockSize.height)
let color = document.stateAt(row: row, column: column)
? UIColor.blackColor() : UIColor.whiteColor()

color.setFill()
tintColor.setStroke()
let path = UIBezierPath(rect:blockFrame)
path.fill()
path.stroke()
}

This code uses the grid origin and the cell size and gap values set by the calculateGridForSize() method to figure out where each cell should be, and then draws it using the current tint color for the outline and either black or white for the interior fill, depending on whether the cell should be filled or not. The methods that are used for drawing will be explained in Chapter 16.

Finally, we add a set of methods that respond to touch events by the user. Both touchesBegan(_, withEvent:) and touchesMoved(_, withEvent:) are standard methods that every UIView subclass can implement to capture touch events that happen within the view’s frame. We’ll discuss these methods in detail in Chapter 19. Our implementation of these two methods uses two other methods we’re adding here to calculate a grid location based on a touch location and to toggle a specific value in the document. Again, these methods use the values set by thecalculateGridForSize() method to decide whether a touch falls within a grid cell or not. Add these four methods at the bottom of the file, just above the closing brace:

private func touchedGridIndexFromTouches(touches: NSSet) -> GridIndex {
var result = GridIndex(row: -1, column: -1)
let touch = touches.anyObject() as UITouch
var location = touch.locationInView(self)
if CGRectContainsPoint(gridRect, location) {
location.x -= gridRect.origin.x
location.y -= gridRect.origin.y
result.column = Int(8 - (location.x * 8.0 / gridRect.size.width))
result.row = Int(location.y * 8.0 / gridRect.size.height)
}
return result
}

private func toggleSelectedBlock() {
if selectedBlockIndex.row != -1
&& selectedBlockIndex.column != -1 {
document.toggleStateAt(row: selectedBlockIndex.row,
column: selectedBlockIndex.column)
document.undoManager?.prepareWithInvocationTarget(document)
.toggleStateAt(row: selectedBlockIndex.row,
column: selectedBlockIndex.column)
setNeedsDisplay()
}
}

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
selectedBlockIndex = touchedGridIndexFromTouches(touches)
toggleSelectedBlock()
}

override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
let touched = touchedGridIndexFromTouches(touches)
if touched.row != selectedBlockIndex.row
&& touched.column != selectedBlockIndex.column {
selectedBlockIndex = touched
toggleSelectedBlock()
}
}

Sharp-eyed readers may have noticed that the toggleSelectedBlock ()method does something a bit special. After calling the document’s toggleStateAt(row:, column:)method to change the value of a particular grid point, it does something more. Let’s take another look:

private func toggleSelectedBlock() {
if selectedBlockIndex.row != -1
&& selectedBlockIndex.column != -1 {
document.toggleStateAt(row: selectedBlockIndex.row,
column: selectedBlockIndex.column)
document.undoManager?.prepareWithInvocationTarget(document)
.toggleStateAt(row: selectedBlockIndex.row,
column: selectedBlockIndex.column)
setNeedsDisplay()
}
}

The call to document.undoManager() returns an instance of NSUndoManager. We haven’t dealt with this directly anywhere else in this book, but NSUndoManager is the structural underpinning for the undo/redo functionality in both iOS and OS X. The idea is that anytime the user performs an action in the GUI, you use NSUndoManager to leave a sort of breadcrumb by “recording” a method call that will undo what the user just did. NSUndoManager will store that method call on a special undo stack, which can be used to backtrack through a document’s state whenever the user activates the system’s undo functionality.

The way it works is that the prepareWithInvocationTarget() method returns a proxy object to which you can send any message, and the message will be packed up with the target and pushed onto the undo stack. So, while it may look like you’re calling toggleStateAt(row:, column:) twice in a row, the second time it’s not being called but instead is just being queued up for later potential use.

So, why are we doing this? We haven’t been giving any thought to undo/redo issues up to this point, so why now? The reason is that registering an undoable action with the document’s NSUndoManager marks the document as “dirty” and ensures that it will be saved automatically at some point in the next few seconds. The fact that the user’s actions are also undoable is just icing on the cake, at least in this application. In an app with a more complex document structure, allowing document-wide undo support can be hugely beneficial.

Save your changes. Now that our view class is ready to go, let’s head back to the storyboard to configure the GUI for the detail view.

Storyboard Detailing

Select Main.storyboard, find the detail scene, and take a look at what’s there right now.

All the GUI contains is a label (“Detail view content goes here”), which is the one that contained the document’s description when you ran the app earlier. That label isn’t particularly useful, so select the label in the detail view controller and press the Delete key to remove it.

Use the object library to find a UIView and drag it into the detail view. Position and size it so that it fills the entire area below the title bar (see Figure 14-5).

image

Figure 14-5. We replaced the label in the detail view with another view, centered in its containing view. The view becomes somewhat invisible while dragging, but here you can see that it’s partly covering the dashed lines that appear when you drag it to the center of the view

Switch over to the Identity Inspector, so we can change this UIView instance into an instance of our custom class. In the Custom Class section at the top of the inspector, select the Class pop-up list, and choose TinyPixView. Now open the Attributes Inspector and change the Mode setting to Redraw. This causes TinyPixView to redraw itself when its size changes. This is necessary because the position of the grid inside the view depends on the size of the view itself, which changes when the device is rotated. At this point, the view hierarchy for the Detail Scene should look likeFigure 14-6.

image

Figure 14-6. The detail view scene’s view hierarchy

Before we go on, we need to adjust the auto layout constraints for the new view. We want it to fill the available area in the detail view. So, in the Document Outline, Control-drag from TinyPixView to its parent view and release the mouse. Hold down the Shift key and in the pop-up, selectLeading Space to Container Margin, Trailing Space to Container Margin, Top Space to Top Layout Guide, and Bottom Space to Bottom Layout Guide, and then click outside the pop-up to apply the constraints.

Now we need to wire up the custom view to our detail view controller. We haven’t prepared an outlet for our custom view yet, but that’s OK since Xcode’s drag-to-code feature will do that for us.

Activate the Assistant Editor. A text editor should slide into place alongside the GUI editor, displaying the contents of DetailViewController.swift. If it’s showing you anything else, use the jump bar at the top of the text editor to make DetailViewController.swift come into view.

To make the connection, Control-drag from the TinyPixView icon in the Document Outline to the code, releasing the drag below the existing IBOutlet at the top of the file. In the pop-up window that appears, make sure that Connection is set to Outlet, name the new outlet pixView, and click the Connect button. While we’re here, delete the detailDescriptionLabel outlet, since we’re not going to be using it.

You should see that making that connection has added this line to DetailViewController.swift:

@IBOutlet weak var detailDescriptionLabel: UILabel!
@IBOutlet weak var pixView: TinyPixView!

Now let’s modify the configureView() method. This isn’t a standard UIViewController method. It’s just a private method that the project template included in this class as a convenient spot to put code that needs to update the view after anything changes. Since we’re not using the description label, we delete the line that sets that. Next, we add a bit of code to pass the chosen document along to our custom view and tell it to redraw itself by calling setNeedsDisplay():

func configureView() {
// Update the user interface for the detail item.
if let detail: AnyObject = self.detailItem {
if let label = self.detailDescriptionLabel {
label.text = detail.description
}
}
if detailItem != nil && isViewLoaded() {
pixView.document = detailItem! as TinyPixDocument
pixView.setNeedsDisplay()
}
}

Notice the call to isViewLoaded() before updating the document in the TinyPixView object. This is needed because it’s possible for configureView() to be called before the detail view controller has loaded its view. In that case, the pixView property will still be nil and the app will crash if we try to use it. We can safely defer updating the document in this case, because configureView() will be called again from viewDidLoad when the view is actually loaded.

Next, we need to arrange for the tint color to be applied to the TinyPixView. We need to do this both when the view is first loaded and whenever the tint color is changed. We know that we can get the initial tint color from the user defaults, so let’s add a method that gets the value saved there, converts it to a UIColor and applies it to the TinyPixView. Add this method somewhere in the body of the class:

private func updateTintColor() {
let prefs = NSUserDefaults.standardUserDefaults()
let selectedColorIndex = prefs.integerForKey("selectedColorIndex")
let tintColor = TinyPixUtils.getTintColorForIndex(selectedColorIndex)
pixView.tintColor = tintColor
pixView.setNeedsDisplay()
}

We need to call this method to set the initial tint color when the view is first loaded. We also need to call it when the tint changes. How will we know that’s happened? When the tint color is changed, the new value is saved in the user defaults. You can find out that something in the user defaults has changed by registering an observer for the NSUserDefaultsDidChangeNotification notification with the default notification center. Add the following code to the viewDidLoad method:

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

updateTintColor()
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "onSettingsChanged:",
name: NSUserDefaultsDidChangeNotification, object: nil)
}

Now, when anything in the user defaults changes, the onSettingsChanged() method is called. When this happens, we need to set the new tint color, in case it’s changed. Add the implementation of this method to the class:

func onSettingsChanged(notification: NSNotification) {
updateTintColor()
}

Having added a notification observer, we have to remove it before the class is deallocated. We can do this by implementing the class deinitializer:

deinit {
NSNotificationCenter.defaultCenter().removeObserver(self,
name: NSUserDefaultsDidChangeNotification, object: nil)
}

We’re nearly finished with this class, but we need to make one more change. Remember when we mentioned the autosaving that takes place when a document is notified that some editing has occurred, triggered by registering an undoable action? The save normally happens within about 10 seconds after the edit occurs. Like the other saving and loading procedures we described earlier in this chapter, it happens in a background thread, so that normally the user won’t even notice. However, that works only as long as the document is still around.

With our current set up, there’s a risk that when the user hits the Back button to go back to the master list, the document instance will be deallocated without any save operation occurring, and the user’s latest changes will be lost. To make sure this doesn’t happen, we need to add some code to the viewWillDisappear() method to close the document as soon as the user navigates away from the detail view. Closing a document causes it to be automatically saved, and again, the saving occurs on a background thread. In this particular case, we don’t need to do anything when the save is done, so we pass in nil instead of a block:

Add this viewWillDisappear() method:

override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if let doc = detailItem as? UIDocument {
doc.closeWithCompletionHandler(nil)
}
}

And with that, this version of our first truly document-based app is ready to try out! Fire it up and bask in the glory. You can create new documents, edit them, flip back to the list, and then select another document (or the same document), and it all just works. Experiment with changing the tint color and verify that it is properly saved and restored when you stop and restart the app. If you open the Xcode console while doing this, you’ll see some output each time a document is loaded or saved. Using the autosaving system, you don’t have direct control over just when saves occur (except for when closing a document), but it can be interesting to watch the logs just to get a feel for when they happen.

Adding iCloud Support

You now have a fully working document-based app, but we’re not going to stop here. We promised you iCloud support in this chapter, and it’s time to deliver!

Modifying TinyPix to work with iCloud is pretty straightforward. Considering all that’s happening behind the scenes, this requires a surprisingly small number of changes. We’ll need to make some revisions to the method that loads the list of available files and the method that specifies the URL for loading a new file, but that’s about it.

Apart from the code changes, we will also need to deal with some additional administrative details. Apple allows an app to save to iCloud only if it contains an embedded provisioning profile that is configured to allow iCloud usage. This means that to add the iCloud support to our app, you must have a paid iOS developer membership and have installed your developer certificate. It also works only with actual devices, not the simulator, so you’ll need to have at least one iOS device registered with iCloud to run the new iCloud-backed TinyPix. With two devices, you’ll have even more fun, as you can see how changes made on one device propagate to the other.

Creating a Provisioning Profile

First, you need to create an iCloud-enabled provisioning profile for TinyPix. This used to require a lot of convoluted steps on Apple’s developer web site, but nowadays Xcode makes it easy. In the Project Navigator, select the TinyPix item at the top, and then click the Capabilities tab in the editing area. You should see something like what’s shown in Figure 14-7.

image

Figure 14-7. Xcode’s presentation of easily configurable app technologies and services

The list of capabilities shown in Figure 14-7 can all be configured directly in Xcode, all without needing to go to a web site, create and download provisioning profiles, and so on. Before you can do this, you need to give your app a unique App ID. If you used the version of the project that’s in the source code download, the App ID is com.apress.BIDSWIFT. This App ID is already registered, so you won’t be able to use it. Select the General tab and use a different prefix in the Bundle Identifier field. To change the App ID to com.myCo, for example, you would set the Bundle Identifier as shown in Figure 14-8.

image

Figure 14-8. Changing the application’s bundle ID

Of course, you should use a value that’s unique to you rather than com.myCo. Now switch back to the Capabilities tab. For TinyPix, we want to enable iCloud, the first capability listed, so click the disclosure triangle next to the cloud icon. Here you’ll see some information about what this capability is for. Click the switch at the right to turn it on. Xcode will then communicate with Apple’s servers to configure the provisioning profile for this app. This will require you to log in with your Apple ID, and it obviously requires you to be connected to the Internet. After it’s enabled, click to turn on the Key-value storage and iCloud Documents check boxes, as shown in Figure 14-9.

image

Figure 14-9. The app is now configured to use iCloud. This simple configuration let us remove several pages from this chapter, which probably ends up saving the life of a tree or two. Thanks, Apple!

You’re finished! Your app now has the necessary permissions to access iCloud from your code. The rest is a simple matter of programming.

How to Query

Select MasterViewController.swift so that we can start making changes for iCloud. The biggest change is going to be the way we look for available documents. In the first version of TinyPix, we used NSFileManager to see what’s available on the local file system. This time, we’re going to do things a little differently. Here, we will fire up a special sort of query to look for documents.

Start by adding a pair of properties to the class: one to hold a pointer to an ongoing query and the other to hold the list of all the documents the query finds.

class MasterViewController: UITableViewController {
@IBOutlet var colorControl: UISegmentedControl!
private var documentFileNames: [String] = []
private var chosenDocument: TinyPixDocument?
private var query: NSMetadataQuery!
private var documentURLs: [NSURL] = []

Now, let’s look at the new file-listing method. Remove the entire reloadFiles() method and replace it with this:

private func reloadFiles() {
let fileManager = NSFileManager.defaultManager()

// Passing nil is OK here, matches the first entitlement
let cloudURL = fileManager.URLForUbiquityContainerIdentifier(nil)
println("Got cloudURL \(cloudURL)")
if (cloudURL != nil) {
query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K like '*.tinypix'",
NSMetadataItemFSNameKey)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]

NSNotificationCenter.defaultCenter().addObserver(self,
selector: "updateUbiquitousDocuments:",
name: NSMetadataQueryDidFinishGatheringNotification,
object: nil)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "updateUbiquitousDocuments:",
name: NSMetadataQueryDidUpdateNotification,
object: nil)

query.startQuery()
}
}

There are some new things here that are definitely worth mentioning. The first is seen in this line:

let cloudURL = fileManager.URLForUbiquityContainerIdentifier(nil)

That’s a mouthful, for sure. Ubiquity? What are we talking about here? When it comes to iCloud, a lot of Apple’s terminology for identifying resources in iCloud storage includes words like “ubiquity” and “ubiquitous” to indicate that something is omnipresent—accessible from any device using the same iCloud login credentials.

In this case, we’re asking the file manager to give us a base URL that will let us access the iCloud directory associated with a particular container identifier. A container identifier is normally a string containing your company’s unique bundle seed ID and the application identifier. The container identifier is used to pick one of the iCloud entitlements contained within your app. Passing nil here is a shortcut that just means “give me the first one in the list.” Since our app contains only one checked item in that list (which you can see listed under “Containers” at the bottom ofFigure 14-9), that shortcut suits our needs perfectly.

After that, we create and configure an instance of NSMetadataQuery:

query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K like '*.tinypix'",
NSMetadataItemFSNameKey)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]

The NSMetaDataQuery class was originally written for use with the Spotlight search facility on OS X, but it’s now doing extra duty as a way to let iOS apps search iCloud directories. We give the query a predicate, which limits its search results to include only those with the correct sort of file name, and we give it a search scope that limits it to look just within the Documents folder in the app’s iCloud storage. Next, we set up some notifications to let us know when the query is complete and then we initiate the query:

NSNotificationCenter.defaultCenter().addObserver(self,
selector: "updateUbiquitousDocuments",
name: NSMetadataQueryDidFinishGatheringNotification,
object: nil)
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "updateUbiquitousDocuments",
name: NSMetadataQueryDidUpdateNotification,
object: nil)

query.startQuery()

Now we need to implement the method that those notifications call when the query is done. Add this method just below the reloadFiles() method:

func updateUbiquitousDocuments(notification: NSNotification) {
documentURLs = []
documentFileNames = []

println("updateUbiquitousDocuments, results = \(query.results)")
let results = sorted(query.results) { obj1, obj2 in
let item1 = obj1 as NSMetadataItem
let item2 = obj2 as NSMetadataItem
let item1Date =
item1.valueForAttribute(NSMetadataItemFSCreationDateKey) as NSDate
let item2Date =
item2.valueForAttribute(NSMetadataItemFSCreationDateKey) as NSDate
let result = item1Date.compare(item2Date)
return result == NSComparisonResult.OrderedAscending
}
for item in results as [NSMetadataItem] {
let url = item.valueForAttribute(NSMetadataItemURLKey) as NSURL
documentURLs.append(url)
documentFileNames.append(url.lastPathComponent)
}
tableView.reloadData()
}

The query’s results contain a list of NSMetadataItem objects, from which we can get items like file URLs and creation dates. We use this to sort the items by date, and then grab all the URLs for later use.

Save Where?

The next change is to the urlForFilename: method, which once again is completely different. Here, we’re using a ubiquitous URL to create a full path URL for a given file name. We insert "Documents" in the generated path as well, to make sure we’re using the app’s Documentsdirectory. Delete the old method and replace it with this new one:

private func urlForFileName(fileName: NSString) -> NSURL {
// Be sure to insert "Documents" into the path
let fm = NSFileManager.defaultManager()
let baseURL = fm.URLForUbiquityContainerIdentifier(nil)
let pathURL = baseURL?.URLByAppendingPathComponent("Documents")
let destinationURL = pathURL?.URLByAppendingPathComponent(fileName)
return destinationURL!
}

Now, build and run your app on an actual iOS device (not the simulator). If you’ve run the previous version of the app on that device, you’ll find that any TinyPix masterpieces you created earlier are now nowhere to be seen. This new version ignores the local Documents directory for the app and relies completely on iCloud. However, you should be able to create new documents and find that they stick around after quitting and restarting the app. Moreover, you can even delete the TinyPix app from your device entirely, run it again from Xcode, and find that all your iCloud-saved documents are available at once. If you have an additional iOS device configured with the same iCloud user, use Xcode to run the app on that device, and you’ll see all the same documents appear there, as well! It’s pretty sweet. You can also find these documents in the iCloud section of your iOS device’s Settings app (look under Storage image Manage Storage image TinyPix), as well as the iCloud section of your Mac’s System Preferences app if you’re running OS X 10.8 or later.

Storing Preferences on iCloud

We can “cloudify” one more piece of functionality with just a bit of effort. iOS’s iCloud support includes a class called NSUbiquitousKeyValueStore, which works a lot like NSUserDefaults; however, its keys and values are stored in the cloud. This is great for application preferences, login tokens, and anything else that doesn’t belong in a document, but could be useful when shared among all of a user’s devices.

In TinyPix, we’ll use this feature to store the user’s preferred highlight color. That way, instead of needing to be configured on each device, the user sets the color once, and it shows up everywhere. Here’s the plan of action:

· Whenever the user changes the tint color, we’ll save the new value in NSUserDefaults and we’ll also save it in the NSUbiquitousKeyValueStore, which will make it available to instances of the application on other devices.

· We’ll register to be notified of changes in the NSUbiquitousKeyValueStore. When we’re notified of a change, we’ll get the new tint color value. At this point, we need to update the segmented control and the tint color used by the master view controller and the drawing color in the detail view controller. Rather than do this directly, we’ll just save the new tint color in NSUserDefaults. Changing NSUserDefaults causes a notification to be generated. The detail view controller is already observing this notification, so it will update itself automatically. We’re going to make some small changes to the master view controller so that it does the same thing.

It’s important to be aware that updates to NSUbiquitousKeyValueStore do not propagate immediately to other devices and, in fact, if a device is not connected to iCloud for any reason, it won’t see the update until it next connects. So don’t expect changes to be seen immediately.

Let’s start by registering to receive change notifications from the iCloud key-value store. Open AppDelegate.swift and add the following code to the application(_, didFinishLaunchingWithOptions:) method:

func application(application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let splitViewController =
self.window!.rootViewController as UISplitViewController
let navigationController = splitViewController.viewControllers[
splitViewController.viewControllers.count-1]
as UINavigationController
navigationController.topViewController.navigationItem.leftBarButtonItem =
splitViewController.displayModeButtonItem()
splitViewController.delegate = self

// Register for notification of iCloud key-value changes
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "iCloudKeysChanged:",
name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification,
object: nil)

// Start iCloud key-value updates
NSUbiquitousKeyValueStore.defaultStore().synchronize()
updateUserDefaultsFromICloud()

return true
}

The first new line of code arranges for the application delegate’s iCloudKeysChanged() method to be called when an NSUbiquitousKeyValueStoreDidChangeExternallyNotification occurs—that is, when iCloud notifies a change in any of the application’s key/value pairs. The synchronize method causes local changes to the NSUbiquitousKeyValueStore to be written to iCloud in the background and notification of remote updates to start. The updateUserDefaultsFromICloud() method, which you’ll see shortly, gets the current state of the selected tint color from the iCloud key-value store, if it’s set, and stores it in the local user defaults, so that it will be used immediately.

Next, add the implementation of the iCloudKeysChanged() and updateUserDefaultsFromCloud() methods:

func iCloudKeysChanged(notification: NSNotification) {
updateUserDefaultsFromICloud()
}

private func updateUserDefaultsFromICloud() {
let values = NSUbiquitousKeyValueStore.defaultStore().dictionaryRepresentation
if values["selectedColorIndex"] != nil {
let selectedColorIndex =
Int(NSUbiquitousKeyValueStore.defaultStore().longLongForKey(
"selectedColorIndex"))
let prefs = NSUserDefaults.standardUserDefaults()
prefs.setInteger(selectedColorIndex, forKey: "selectedColorIndex")
prefs.synchronize()
}
}

When a notification occurs, we use the longLongForKey() method to get the new selected tint color index from the key store. The API is very similar to that of NSUserDefaults, but there is no method to store an integer value, so we treat the tint color index as a long long instead. Once we have the value, we simply copy it to the NSUserDefaults and synchronize the change, so that a notification is generated. We already know that the detail view controller will update itself when it receives this notification. Next, we need to change the master view controller so that it does the same. Back in MasterViewController.swift, start by registering the controller to be notified of NSUserDefaults changes in its viewDidLoad() method:

reloadFiles()

NSNotificationCenter.defaultCenter().addObserver(self,
selector: "onSettingsChanged:",
name: NSUserDefaultsDidChangeNotification ,
object: nil)
}

Next, add the onSettingsChanged: method:

func onSettingsChanged(notification: NSNotification) {
let prefs = NSUserDefaults.standardUserDefaults()
let selectedColorIndex = prefs.integerForKey("selectedColorIndex")
setTintColorForIndex(selectedColorIndex)
colorControl.selectedSegmentIndex = selectedColorIndex
}

This method updates the tint color of the segmented control using the same method that’s called when the user taps one of its segments, but it gets the color index from NSUserDefaults instead of from the control.

Finally, when the user changes the tint color, we need to save the new index in the iCloud key-value store. Make the following changes to the chooseColor() method to take care of this:

@IBAction func chooseColor(sender: UISegmentedControl) {
let selectedColorIndex = sender.selectedSegmentIndex
setTintColorForIndex(selectedColorIndex)

let prefs = NSUserDefaults.standardUserDefaults()
prefs.setInteger(selectedColorIndex, forKey: "selectedColorIndex")
prefs.synchronize()

NSUbiquitousKeyValueStore.defaultStore()
.setLongLong(Int64(selectedColorIndex),
forKey: "selectedColorIndex")
NSUbiquitousKeyValueStore.defaultStore().synchronize()
}

That’s it! You can now run the app on multiple devices configured for the same iCloud user and will see that setting the color on one device results in the new color appearing on the other device soon afterwards. Piece of cake!

What We Didn’t Cover

We now have the basics of an iCloud-enabled, document-based application up and running, but there are a few more issues that you may want to consider. We’re not going to cover these topics in this book; but if you’re serious about making a great iCloud-based app, you’ll want to think about these areas:

· Documents stored in iCloud are prone to conflicts. What happens if you edit the same TinyPix file on several devices at once? Fortunately, Apple has already thought of this and provides some ways to deal with these conflicts in your app. It’s up to you to decide whether you want to ignore conflicts, try to fix them automatically, or ask the user to help sort out the problem. For full details, search for a document titled “Resolving Document Version Conflicts” in the Xcode documentation viewer.

· Apple recommends that you design your application to work in a completely offline mode in case the user isn’t using iCloud for some reason. It also recommends that you provide a way for a user to move files between iCloud storage and local storage. Sadly, Apple doesn’t provide or suggest any standard GUI for helping a user manage this, and current apps that provide this functionality, such as Apple’s iWork apps, don’t seem to handle it in a particularly user-friendly way. See Apple’s “Managing the Life Cycle of a Document” in the Xcode documentation for more on this.

· Apple supports using iCloud for Core Data storage and even provides a class called UIManagedDocument that you can subclass if you want to make that work. See the UIManagedDocument class reference for more information. This architecture is a lot more complex and problematic than normal iCloud document storage. Apple has taken steps to improve things in recent versions of iOS, but it’s still not perfectly smooth, so look before you leap.

What’s up next? In Chapter 15, we’ll take you through the process of making sure your apps work properly in a multithreaded, multitasking environment.