Documents and iCloud - Beginning iPhone Development: Exploring the iOS SDK, Seventh Edition (2014)

Beginning iPhone Development: Exploring the iOS SDK, Seventh Edition (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 then 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 Nextagain 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 files.

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, let’s give it a method that takes a pair of row and column indexes and returns a BOOLvalue. Let’s 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.

Select TinyPixDocument.h to edit the new class’s header. Add the following bold lines:

#import <UIKit/UIKit.h>

@interface TinyPixDocument : UIDocument

// row and column range from 0 to 7
- (BOOL)stateAtRow:(NSUInteger)row column:(NSUInteger)column;
- (void)setState:(BOOL)state atRow:(NSUInteger)row column:(NSUInteger)column;
- (void)toggleStateAtRow:(NSUInteger)row column:(NSUInteger)column;

@end

Now switch over to TinyPixDocument.m, where we’ll implement storage for our 8 × 8 grid, the methods defined in 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 instance of NSMutableData, which lets us work directly with an array of byte data that is still contained inside an object, so that the usual Cocoa memory management will take care of freeing the memory when we’re finished with it. Add this class extension to make it happen:

#import "TinyPixDocument.h"

@interface TinyPixDocument ()

@property (strong, nonatomic) NSMutableData *bitmap;

@end

@implementation TinyPixDocument

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, placing it directly above the @end at the bottom of the file:

- (id)initWithFileURL:(NSURL *)url {
self = [super initWithFileURL:url];
if (self) {
unsigned char startPattern[] = {
0x01,
0x02,
0x04,
0x08,
0x10,
0x20,
0x40,
0x80
};

self.bitmap = [NSMutableData dataWithBytes:startPattern length:8];
}
return self;
}

This starts off each 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 we defined in the header. Let’s tackle the method for reading the state of a single bit first. 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 YES or NO accordingly. Add this method above the @end:

- (BOOL)stateAtRow:(NSUInteger)row column:(NSUInteger)column {
const char *bitmapBytes = [self.bitmap bytes];
char rowByte = bitmapBytes[row];
char result = (1 << column) & rowByte;
if (result != 0) {
return YES;
} else {
return NO;
}
}

Next comes the inverse: a method that sets the value specified at a given row and column. Here, we once again grab the relevant 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 above the @end:

- (void)setState:(BOOL)state atRow:(NSUInteger)row column:(NSUInteger)column {
char *bitmapBytes = [self.bitmap mutableBytes];
char *rowByte = &bitmapBytes[row];

if (state) {
*rowByte = *rowByte | (1 << column);
} else {
*rowByte = *rowByte & ~(1 << column);
}
}

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

- (void)toggleStateAtRow:(NSUInteger)row column:(NSUInteger)column {
BOOL state = [self stateAtRow:row column:column];
[self 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 theinitWithFileURL: method 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. Because our document’s internal structure is already contained in an NSMutableData object, which is a subclass of NSData, these implementations are pleasingly simple. Add these two methods above the @end:

- (id)contentsForType:(NSString *)typeName error:(NSError **)outError {
NSLog(@"saving document to URL %@", self.fileURL);
return [self.bitmap copy];
}

- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName
error:(NSError **)outError {
NSLog(@"loading document from URL %@", self.fileURL);
self.bitmap = [contents mutableCopy];
return true;
}

The first of these methods, contentsForType:error:, is called whenever our document is about to be saved to storage. It simply returns an immutable copy of our bitmap data, 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 mutable copy of the data 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.m. 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 all the methods from the@implementation block and all the declarations in the class extension at the top. When you’re done, you should have a clean slate that looks something like this:

#import "MasterViewController.h"
#import "DetailViewController.h"

@interface MasterViewController ()
@end

@implementation MasterViewController
@end

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.m:

#import "MasterViewController.h"
#import "DetailViewController.h"
#import "TinyPixDocument.h"

@interface MasterViewController ()

@property (weak, nonatomic) IBOutlet UISegmentedControl *colorControl;
@property (strong, nonatomic) NSArray *documentFilenames;
@property (strong, nonatomic) TinyPixDocument *chosenDocument;

@end

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 the implementation, placing it directly above the @end at the bottom of the file:

- (NSURL *)urlForFilename:(NSString *)filename {
NSFileManager *fm = [NSFileManager defaultManager];
NSArray *urls = [fm URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask];
NSURL *directoryURL = urls[0];
NSURL *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 method above the @end:

- (void)reloadFiles {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *path = paths[0];
NSFileManager *fm = [NSFileManager defaultManager];

NSError *dirError;
NSArray *files = [fm contentsOfDirectoryAtPath:path error:&dirError];
if (!files) {
NSLog(@"Error listing files in directory %@: %@",
path, dirError);
}
NSLog(@"found files: %@", files);

files = [files sortedArrayUsingComparator:
^NSComparisonResult(id filename1, id filename2) {
NSDictionary *attr1 = [fm attributesOfItemAtPath:
[path stringByAppendingPathComponent:filename1]
error:nil];
NSDictionary *attr2 = [fm attributesOfItemAtPath:
[path stringByAppendingPathComponent:filename2]
error:nil];
return [attr2[NSFileCreationDate] compare: attr1[NSFileCreationDate]];
}];
self.documentFilenames = files;
[self.tableView reloadData];
}

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 above the @end:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}

- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return [self.documentFilenames count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
@"FileCell"];

NSString *path = self.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 above the @end:

- (IBAction)chooseColor:(id)sender {
NSInteger selectedColorIndex = [(UISegmentedControl *)sender
selectedSegmentIndex];
[self setTintColorForIndex:selectedColorIndex];

NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setInteger:selectedColorIndex forKey:@"selectedColorIndex"];
[prefs synchronize];
}

- (void)setTintColorForIndex:(NSInteger)selectedColorIndex {
self.colorControl.tintColor = [TinyPixUtils getTintColorForIndex:selectedColorIndex];
}

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 Cocoa Touch Class and click Next. Enter TinyPixUtils in the Class field, enter NSObject in the Subclass of field, and click Next. Finally, click Create to create the files.

The TinyPixUtils class will have a single method. Edit TinyPixUtils.h to add the declaration of that method:

#import <UIKit/UIKit.h>

@interface TinyPixUtils : NSObject

+ (UIColor *)getTintColorForIndex:(NSUInteger)index;

@end

Now switch over to TinyPixUtils.m to add the method implementation:

#import "TinyPixUtils.h"

@implementation TinyPixUtils

+ (UIColor *)getTintColorForIndex:(NSUInteger)index {
UIColor *color = [UIColor redColor];
switch (index) {
case 0:
color = [UIColor redColor];
break;
case 1:
color =
[UIColor colorWithRed:0 green:0.6 blue:0 alpha:1];
break;
case 2:
color = [UIColor blueColor];
break;
default:
break;
}
return color;
}

@end

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.m. Start by adding an import for TinyPixUtils.h:

#import "MasterViewController.h"
#import "DetailViewController.h"
#import "TinyPixDocument.h"
#import "TinyPixUtils.h"

@interface MasterViewController ()

Now let’s work on the viewDidLoad method. After calling the superclass’s implementation, we’ll start by adding 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:

- (void)viewDidLoad {
[super viewDidLoad];

UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
target:self
action:@selector(insertNewObject)];
self.navigationItem.rightBarButtonItem = addButton;

NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
NSInteger selectedColorIndex = [prefs integerForKey:@"selectedColorIndex"];
[self setTintColorForIndex:selectedColorIndex];
[self.colorControl setSelectedSegmentIndex:selectedColorIndex];

[self 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 above the @end:

- (void)insertNewObject {
UIAlertController *alert =
[UIAlertController alertControllerWithTitle:@"Choose File Name"
message: @"Enter a name for your new TinyPix document."
preferredStyle: UIAlertControllerStyleAlert];

[alert addTextFieldWithConfigurationHandler:nil];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel handler:nil];
UIAlertAction *createAction = [UIAlertAction actionWithTitle:@"Create"
style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
UITextField *textField = (UITextField *)alert.textFields[0];
[self createFileNamed:textField.text];
}];
[alert addAction:cancelAction];
[alert addAction:createAction];

[self presentViewController:alert animated:YES 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 address now. Add this method above the @end:

- (void)createFileNamed:(NSString *)fileName {
NSString *trimmedFileName = [fileName
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (trimmedFileName.length > 0) {
NSString *targetName = [NSString stringWithFormat:@"%@.tinypix",
trimmedFileName];
NSURL *saveUrl = [self urlForFilename:targetName];
self.chosenDocument =
[[TinyPixDocument alloc] initWithFileURL:saveUrl];
[self.chosenDocument saveToURL:saveUrl
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
if (success) {
NSLog(@"save OK");
[self reloadFiles];
[self performSegueWithIdentifier:@"masterToDetail"
sender:self];
} else {
NSLog(@"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 the initWithFileURL: 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:

[self.chosenDocument saveToURL:saveUrl
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
.
.
.
}];

Of interest is the purpose and usage of the block 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 block 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 block 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) {
NSLog(@"save OK");
[self reloadFiles];
[self performSegueWithIdentifier:@"masterToDetail"
sender:self];
} else {
NSLog(@"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. Insert this method above the @end:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
UINavigationController *destination =
(UINavigationController *)segue.destinationViewController;
DetailViewController *detailVC =
(DetailViewController *)destination.topViewController;
if (sender == self) {
// if sender == self, a new document has just been created,
// and chosenDocument is already set.
detailVC.detailItem = self.chosenDocument;
} else {
// find the chosen document from the tableview
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
NSString *filename = self.documentFilenames[indexPath.row];
NSURL *docUrl = [self urlForFilename:filename];
self.chosenDocument = [[TinyPixDocument alloc]
initWithFileURL:docUrl];
[self.chosenDocument openWithCompletionHandler:^(BOOL success) {
if (success) {
NSLog(@"load OK");
detailVC.detailItem = self.chosenDocument;
} else {
NSLog(@"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 block that it will save for later execution. Just as with the file-saving method, the loading occurs in the background, and this block 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 the description of itsdetailItem 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 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.h and make the following changes:

#import <UIKit/UIKit.h>

@class TinyPixDocument;

@interface TinyPixView : UIView

@property (strong, nonatomic) TinyPixDocument *document;

@end

All we’re doing here is adding a property, so that the controller can pass along the document.

Now switch over to TinyPixView.m, where we have some more substantial work ahead of us. Start by adding this class extension at the top of the file:

#import "TinyPixView.h"
#import "TinyPixDocument.h"

typedef struct {
NSUInteger row;
NSUInteger column;
} GridIndex;

@interface TinyPixView ()

@property (assign, nonatomic) CGSize lastSize;
@property (assign, nonatomic) CGRect gridRect;
@property (assign, nonatomic) CGSize blockSize;
@property (assign, nonatomic) CGFloat gap;
@property (assign, nonatomic) GridIndex selectedBlockIndex;

@end

@implementation TinyPixView
.
.
.

Here, we defined a C struct called GridIndex as a handy way to deal with row/column pairs. We also defined a class extension with some properties that we’ll need to use later.

A UIView subclass is usually initialized by calling its initWithFrame: 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 initWithCoder: method. We’ll implement both of these methods, making each call a third method that initializes our properties. Add the following code to TinyPixView.m:

- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
[self commonInit];
}
return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonInit];
}
return self;
}

- (void)commonInit{
[self calculateGridForSize:self.bounds.size];
_selectedBlockIndex.row = NSNotFound;
_selectedBlockIndex.column = NSNotFound;
}

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.m:

- (void)calculateGridForSize:(CGSize)size {
CGFloat space = MIN(size.width, size.height);
_gap = space/57;
CGFloat cellSide = 6 * _gap;
_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.
- (void)drawRect:(CGRect)rect
{
// Drawing code
if (!_document) return;

CGSize size = self.bounds.size;
if (!CGSizeEqualToSize(size, self.lastSize)) {
self.lastSize = size;
[self calculateGridForSize:size];
}

for (NSUInteger row = 0; row < 8; row++) {
for (NSUInteger column = 0; column < 8; column++) {
[self drawBlockAtRow: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:

- (void)drawBlockAtRow:(NSUInteger)row column:(NSUInteger)column {
CGFloat startX = _gridRect.origin.x + _gap
+ (_blockSize.width + _gap) * (7 - column) + 1;
CGFloat startY = _gridRect.origin.y + _gap
+ (_blockSize.height + _gap) * row + 1;
CGRect blockFrame = CGRectMake(startX, startY,
_blockSize.width, _blockSize.height);
UIColor *color = [_document stateAtRow:row column:column] ?
[UIColor blackColor] : [UIColor whiteColor];
[color setFill];
[self.tintColor setStroke];
UIBezierPath *path = [UIBezierPath bezierPathWithRect: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 @end:

- (GridIndex)touchedGridIndexFromTouches:(NSSet *)touches {
GridIndex result;
result.row = -1;
result.column = -1;
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
if (CGRectContainsPoint(_gridRect, location)) {
location.x -= _gridRect.origin.x;
location.y -= _gridRect.origin.y;
result.column = 8 - (location.x * 8.0 / _gridRect.size.width);
result.row = location.y * 8.0 / _gridRect.size.height;
}
return result;
}

- (void)toggleSelectedBlock {
if (_selectedBlockIndex.row != -1 && _selectedBlockIndex.column != -1) {
[_document toggleStateAtRow:_selectedBlockIndex.row
column:_selectedBlockIndex.column];
[[_document.undoManager prepareWithInvocationTarget:_document]
toggleStateAtRow:_selectedBlockIndex.row column:_selectedBlockIndex.column];
[self setNeedsDisplay];
}
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.selectedBlockIndex = [self touchedGridIndexFromTouches:touches];
[self toggleSelectedBlock];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
GridIndex touched = [self touchedGridIndexFromTouches:touches];
if (touched.row != _selectedBlockIndex.row
|| touched.column != _selectedBlockIndex.column) {
_selectedBlockIndex = touched;
[self toggleSelectedBlock];
}
}

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

- (void)toggleSelectedBlock {
if (_selectedBlockIndex.row != -1 && _selectedBlockIndex.column != -1) {
[_document toggleStateAtRow:_selectedBlockIndex.row
column:_selectedBlockIndex.column];
[[_document.undoManager prepareWithInvocationTarget:_document]
toggleStateAtRow:_selectedBlockIndex.row
column:_selectedBlockIndex.column];
[self 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 callingtoggleStateAtRow:column: twice in a row, the second time it’s not being called but instead is just being queued up for later potential use. This kind of spectacularly dynamic behavior is an area where Objective-C really stands out in comparison to static languages such as C++, where techniques such as letting one object act as a proxy to another or packing up a method invocation for later use have no language support and are nearly impossible (and therefore many tasks, such as building undo support, can be quite tedious).

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.m. If it’s showing you anything else, use the jump bar at the top of the text editor to make DetailViewController.m come into view.

To make the connection, Control-drag from the TinyPixView icon in the Document Outline to the code, releasing the drag in the class extension 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 theConnect button.

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

@property (weak, nonatomic) IBOutlet TinyPixView *pixView;

One thing it didn’t add, however, is any knowledge of our custom view class to the source code. Let’s take care of that by adding this line toward the top of DetailViewController.m:

#import "DetailViewController.h"
#import "TinyPixView.h"

@interface DetailViewController ()

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:

- (void)configureView
{
// Update the user interface for the detail item.

if (self.detailItem) {
self.detailDescriptionLabel.text = [self.detailItem description];
self.pixView.document = self.detailItem;
[self.pixView setNeedsDisplay];
}
}

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. The conversion requires the TinyPixUtils class that we created earlier, so first add an import for that class at the top of the file:

#import "DetailViewController.h"
#import "TinyPixView.h"
#import "TinyPixUtils.h"

Next, add this method somewhere in the body of the class:

- (void)updateTintColor {
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
NSInteger selectedColorIndex = [prefs integerForKey:@"selectedColorIndex"];
UIColor *tintColor = [TinyPixUtils getTintColorForIndex:selectedColorIndex];
self.pixView .tintColor = tintColor;
[self.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:

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self configureView];
[self updateTintColor];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@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 above the @end in the class:

- (void)onSettingsChanged:(NSNotification *)notification {
[self updateTintColor];
}

Having added a notification observer, we have to remove it before the class is deallocated. We can do this by overriding the view’s dealloc method:

- (void)dealloc {
[[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 setup, 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:

- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
UIDocument *doc = self.detailItem;
[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.BID. 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.m 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 in the class extension: one to hold a pointer to an ongoing query and the other to hold the list of all the documents the query finds.

@interface MasterViewController ()

@property (weak, nonatomic) IBOutlet UISegmentedControl *colorControl;
@property (strong, nonatomic) NSArray *documentFilenames;
@property (strong, nonatomic) TinyPixDocument *chosenDocument;
@property (strong, nonatomic) NSMetadataQuery *query;
@property (strong, nonatomic) NSMutableArray *documentURLs;

@end

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

- (void)reloadFiles {
NSFileManager *fileManager = [NSFileManager defaultManager];
// passing nil is OK here, matches first entitlement
NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:nil];
NSLog(@"got cloudURL %@", cloudURL); // returns nil in simulator
if (cloudURL != nil) {
self.query = [[NSMetadataQuery alloc] init];
_query.predicate = [NSPredicate predicateWithFormat:@"%K like '*.tinypix'",
NSMetadataItemFSNameKey];
_query.searchScopes = [NSArray arrayWithObject:
NSMetadataQueryUbiquitousDocumentsScope];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(updateUbiquitousDocuments:)
name:NSMetadataQueryDidFinishGatheringNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@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:

NSURL *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:

self.query = [[NSMetadataQuery alloc] init];
_query.predicate = [NSPredicate predicateWithFormat:@"%K like '*.tinypix'",
NSMetadataItemFSNameKey];
_query.searchScopes = [NSArray arrayWithObject:
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:@selector(updateUbiquitousDocuments:)
name:NSMetadataQueryDidFinishGatheringNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@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:

- (void)updateUbiquitousDocuments:(NSNotification *)notification {
self.documentURLs = [NSMutableArray array];
self.documentFilenames = [NSMutableArray array];

NSLog(@"updateUbiquitousDocuments, results = %@", self.query.results);
NSArray *results = [self.query.results sortedArrayUsingComparator:
^NSComparisonResult(id obj1, id obj2) {
NSMetadataItem *item1 = obj1;
NSMetadataItem *item2 = obj2;
return [[item2 valueForAttribute:NSMetadataItemFSCreationDateKey]
compare:
[item1 valueForAttribute:NSMetadataItemFSCreationDateKey]];
}];

for (NSMetadataItem *item in results) {
NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
[self.documentURLs addObject:url];
[(NSMutableArray *)_documentFilenames addObject:[url lastPathComponent]];
}

[self.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:

- (NSURL *)urlForFilename:(NSString *)filename {
// be sure to insert "Documents" into the path
NSURL *baseURL = [[NSFileManager defaultManager]
URLForUbiquityContainerIdentifier:nil];
NSURL *pathURL = [baseURL URLByAppendingPathComponent:@"Documents"];
NSURL *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.m and add the following code to the application:didFinishLaunchingWithOptions: method:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
UISplitViewController *splitViewController =
(UISplitViewController *)self.window.rootViewController;
UINavigationController *navigationController =
[splitViewController.viewControllers lastObject];
navigationController.topViewController.navigationItem.leftBarButtonItem =
splitViewController.displayModeButtonItem;
splitViewController.delegate = self;

// Register for notification of iCloud key-value changes
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(iCloudKeysChanged:)
name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
object:nil];

// Start iCloud key-value updates
[[NSUbiquitousKeyValueStore defaultStore] synchronize];
[self updateUserDefaultsFromICloud];

return YES;
}

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:

- (void)iCloudKeysChanged:(NSNotification *)notification {
[self updateUserDefaultsFromICloud];
}

- (void)updateUserDefaultsFromICloud {
NSDictionary *values = [[NSUbiquitousKeyValueStore defaultStore] dictionaryRepresentation];
if ([values valueForKey:@"selectedColorIndex"] != nil) {
NSUInteger selectedColorIndex = (NSUInteger)[[NSUbiquitousKeyValueStore defaultStore] longLongForKey:@"selectedColorIndex"];
NSUserDefaults *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.m, start by registering the controller to be notified of NSUserDefaults changes in its viewDidLoad method:

[self reloadFiles];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onSettingsChanged:)
name:NSUserDefaultsDidChangeNotification object:nil];
}

Next, add the onSettingsChanged: method:

- (void)onSettingsChanged:(NSNotification *)notification {
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
NSInteger selectedColorIndex = [prefs integerForKey:@"selectedColorIndex"];
[self setTintColorForIndex:selectedColorIndex];
self.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)chooseColor:(id)sender {
NSInteger selectedColorIndex = [(UISegmentedControl *)sender
selectedSegmentIndex];
[self setTintColorForIndex:selectedColorIndex];

NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setInteger:selectedColorIndex forKey:@"selectedColorIndex"];
[prefs synchronize];
[[NSUbiquitousKeyValueStore defaultStore]
setLongLong: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 afterward. 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.