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.

9781430260226_Fig14-01.jpg

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 iPhone. Make sure the Use Core Data checkbox 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 BIDAppDelegate, BIDMasterViewController, and BIDDetailViewController, as well as the Main.storyboard file. We’ll make changes to most of these files to some extent, and we will create a few new classes along the way, as well.

Creating BIDTinyPixDocument

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 Cocoa Touch section, select Objective-C class and click Next. Enter BIDTinyPixDocument 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 BOOL value. 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 BIDTinyPixDocument.h to edit the new class’s header. Add the following bold lines:

#import <UIKit/UIKit.h>
 
@interface BIDTinyPixDocument : 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 BIDTinyPixDocument.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 "BIDTinyPixDocument.h"
 
@interface BIDTinyPixDocument ()
@property (strong, nonatomic) NSMutableData *bitmap;
@end
 
@implementation BIDTinyPixDocument

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 amount 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 a pointer to 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 the convenience method, which lets outside code simply toggle a single cell. Add this method above the @end:

- (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 the initWithFileURL: 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 BIDMasterViewController 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 BIDMasterViewController.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 "BIDMasterViewController.h"
#import "BIDDetailViewController.h"
 
@interface BIDMasterViewController ()
@end
 
@implementation BIDMasterViewController
@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 filenames and a pointer to the document the user has chosen. Make these changes to BIDMasterViewController.m:

#import "BIDMasterViewController.h"
#import "BIDDetailViewController.h"
#import "BIDTinyPixDocument.h"
 
@interface BIDMasterViewController ()<UIAlertViewDelegate>
 
@property (weak, nonatomic) IBOutlet UISegmentedControl *colorControl;
@property (strong, nonatomic) NSArray *documentFilenames;
@property (strong, nonatomic) BIDTinyPixDocument *chosenDocument;
 
@end
.
.
.

Before we implement the table view methods and other standard methods 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. 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;
}

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 pre-existing 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 app. This is a new feature of iOS 7 that lets you define the highlight color for portions of your app or the entire app at once. The UIView class has a tintColor property; and when it’s set for any view, the value will propagate down to all subviews. That means that, by setting the value on the app’s UIWindow (each app has just one), every subview in the app will get the same value.

At the same time, we’ll store the value in NSUserDefaults for later retrieval. Here’s the action method that will do that by passing along the segmented control’s chosen index, along with a method that actually sets the color in the uppermost view. Add these 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 {
    switch (selectedColorIndex) {
        case 0:
            self.view.window.tintColor = [UIColor redColor];
            break;
        case 1:
            self.view.window.tintColor = [UIColor colorWithRed:0
                                                         green:0.6
                                                          blue:0
                                                         alpha:1];
            break;
        case 2:
            self.view.window.tintColor = [UIColor blueColor];
            break;
        default:
            break;
    }
}

We realize that we haven’t yet set this up in the storyboard, but we’ll get there! We’ll also need a method to make sure that the segmented control in our app’s GUI will show the current tint color value from NSUserDefaults as soon as it’s about to be displayed. The best place to put this is in viewDidAppear: because, when that method is called, our view is already in its window. This means we can access the top-level object to set the color:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    NSInteger selectedColorIndex = [prefs integerForKey:@"selectedColorIndex"];
    [self setTintColorForIndex:selectedColorIndex];
    [self.colorControl setSelectedSegmentIndex:selectedColorIndex];
}

Now let’s create a new 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 finish by calling the reloadFiles method that we implemented earlier. Make this change to viewDidLoad:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
        target:self
        action:@selector(insertNewObject)];
    self.navigationItem.rightBarButtonItem = addButton;
    
    [self reloadFiles];
}

You may have noticed that, when we created the UIBarButtonItem in this method, 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 {
    // get the name
    UIAlertView *alert =
    [[UIAlertView alloc] initWithTitle:@"Filename"
                               message:
     @"Enter a name for your new TinyPix document."
                              delegate:self
                     cancelButtonTitle:@"Cancel"
                     otherButtonTitles:@"Create", nil];
    alert.alertViewStyle = UIAlertViewStylePlainTextInput;
    [alert show];
}

This method creates an alert panel that includes a text-input field and displays it. The responsibility of creating a new item instead falls to the delegate method that the alert view calls when it’s finished, which we’ll also address now. Add this method above the @end:

- (void)alertView:(UIAlertView *)alertView
        didDismissWithButtonIndex:(NSInteger)buttonIndex {
    if (buttonIndex == 1) {
        NSString *filename = [NSString stringWithFormat:@"%@.tinypix",
                              [alertView textFieldAtIndex:0].text];
        NSURL *saveUrl = [self urlForFilename:filename];
        self.chosenDocument = [[BIDTinyPixDocument 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 checks the value of buttonIndex to see which button was pressed (a “0” indicates that the user pressed the Cancel button). 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 BIDTinyPixDocument 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 10, 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 {
    if (sender == self) {
        // if sender == self, a new document has just been created,
        // and chosenDocument is already set.
        
        UIViewController *destination = segue.destinationViewController;
        if ([destination respondsToSelector:@selector(setDetailItem:)]) {
            [destination setValue:self.chosenDocument forKey:@"detailItem"];
        }
    } 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 = [[BIDTinyPixDocument alloc]
                               initWithFileURL:docUrl];
        [self.chosenDocument openWithCompletionHandler:^(BOOL success) {
            if (success) {
                NSLog(@"load OK");
                UIViewController *destination = segue.destinationViewController;
                if ([destination respondsToSelector:@selector(setDetailItem:)]) {
                    [destination setValue:self.chosenDocument
                                   forKey:@"detailItem"];
                }
            } 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 10 that this method is called on a view controller whenever a new controller is about to be pushed onto the navigation stack. The sender parameter points out 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 itself. 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, since BIDDetailViewController—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 configure 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 the navigation controller, the master view controller, and the detail view controller (see Figure 14-2). You can ignore the navigation controller entirely, since all our work will be with the other two.

9781430260226_Fig14-02.jpg

Figure 14-2. The TinyPix storyboard, showing the navigation controller, 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 (see Chapter 10 if you need a refresher on the difference between these two cell types). 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. Set the cell’s Identifier 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 detail view controller’s icon (an orange circle at the bottom of its scene or the Master View Controller - Master icon in the dock) over to the detail view controller, and then selecting Push 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, and use the attributes inspector to set its Identifier 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).

9781430260226_Fig14-03.jpg

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 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 orange circle below the controller or the dock icon labeled Master View Controller – Master) 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).

9781430260226_Fig14-04.jpg

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 plus 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 its detailItem in a label. Of course, there’s more information in the console pane. 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 BIDTinyPixView

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 Cocoa Touch section, select Objective-C class and click Next. Name the new class BIDTinyPixView and choose UIView in the Subclass of popup. 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 BIDTinyPixView.h and make the following changes:

#import <UIKit/UIKit.h>
@class BIDTinyPixDocument;
 

@interface BIDTinyPixView : UIView
 
@property (strong, nonatomic) BIDTinyPixDocument *document;
 
@end

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

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

#import "BIDTinyPixView.h"
#import "BIDTinyPixDocument.h"
 
typedef struct {
    NSUInteger row;
    NSUInteger column;
} GridIndex;
 
@interface BIDTinyPixView ()
 
@property (assign, nonatomic) CGSize blockSize;
@property (assign, nonatomic) CGSize gapSize;
@property (assign, nonatomic) GridIndex selectedBlockIndex;
 
@end
 
@implementation BIDTinyPixView
.
.
.

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.

The default emptyUIView subclass contains an initWithFrame: method, which is really the default initializer for the UIView class. 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, making each call a third method that initializes our properties. Make this change to initWithFrame: and add the code just below it:

- (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{
    _blockSize = CGSizeMake(34, 34);
    _gapSize = CGSizeMake(5, 5);
    _selectedBlockIndex.row = NSNotFound;
    _selectedBlockIndex.column = NSNotFound;
}

The _blockSize and _gapSize values are specifically tuned to a view that’s 310 points across. If we wanted to be extra clever here, we could have defined them dynamically based on the view’s actual frame; however, this is the simplest approach that works for our case, so we’re sticking with it!

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 for each 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;
    
    for (NSUInteger row = 0; row < 8; row++) {
        for (NSUInteger column = 0; column < 8; column++) {
            [self drawBlockAtRow:row column:column];
        }
    }
}
*/
 
- (void)drawBlockAtRow:(NSUInteger)row column:(NSUInteger)column {
    CGFloat startX = (_blockSize.width + _gapSize.width) * (7 - column) + 1;
    CGFloat startY = (_blockSize.height + _gapSize.height) * 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];
}

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. These two methods use 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. Add these four methods at the bottom of the file, just above the @end:

- (GridIndex)touchedGridIndexFromTouches:(NSSet *)touches {
    GridIndex result;
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self];
    result.column = 8 - (location.x * 8.0 / self.bounds.size.width);
    result.row = location.y * 8.0 / self.bounds.size.height;
    return result;
}
 
- (void)toggleSelectedBlock {
    [_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 {
    [_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 Mac 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 special kind of 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 toggleStateAtRow: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 undoManager 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. Interface Builder will help you line it up so that it fills the entire area. After dropping it there, use the size inspector to set both its width and height to 310. Finally, drag the view and use the guidelines to center it in its container (see Figure 14-5).

9781430260226_Fig14-05.jpg

Figure 14-5. We replaced the label in the detail view with another view, 310 × 310 pixels, 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 BIDTinyPixView.

Before we go on, we need to adjust the constraints for this new view. We want it to stay centered no matter what the screen size is (so it works well on both the iPhone 4 and iPhone 5 series), but we also want it to maintain its size, as well. So, with the view still selected, click the Align icon at the bottom of the editing area to make its options appear. Click the checkboxes for Horizontal Center in Container and Vertical Center in Container, and then click the Add 2 Constraints button. Next, click the Pin icon at the bottom of the editing area to bring up its popup controller. Click the Width and Height checkboxes, and then click the Add 2 Constraints button to make it happen.

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

To make the connection, control-drag from the Tiny Pix View to the code, releasing the drag in the class extension at the top of the file, somewhere around the configureView declaration. 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.

You should see that making those connections has added this line to BIDDetailViewController.m:

@property (weak, nonatomic) IBOutlet BIDTinyPixView *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 BIDDetailViewController.m:

#import "BIDDetailViewController.h"
#import "BIDTinyPixView.h"
 
@interface BIDDetailViewController ()
.
.
.

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];
    }
}

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:

- (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. 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 website, but Xcode 5 makes this 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-6.

9781430260226_Fig14-06.jpg

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

The list of capabilities shown in Figure 14-6 can all be configured directly in Xcode, all without needing to go to a website, create and download provisioning profiles, and so on. 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 store checkbox, as shown in Figure 14-7.

9781430260226_Fig14-07.jpg

Figure 14-7. 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 BIDMasterViewController.m, so 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 BIDMasterViewController () <UIAlertViewDelegate>
 
@property (weak, nonatomic) IBOutlet UISegmentedControl *colorControl;
@property (strong, nonatomic) NSArray *documentFilenames;
@property (strong, nonatomic) BIDTinyPixDocument *chosenDocument;
@property (strong, nonatomic) NSMetadataQuery *query;
@property (strong, nonatomic) NSMutableArray *documentURLs;
- (NSURL *)urlForFilename:(NSString *)filename;
- (void)reloadFiles;
@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
    
    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 item in that list (created in the previous section), 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];

This class was originally written for use with the Spotlight search facility on Mac 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, so we can fire up the query.

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 Documents directory. 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, 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 an NSDictionary (or NSUserDefaults, for that matter); 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.

Select BIDMasterViewController.m, so we can make a couple of small changes. First, find chooseColor: and make the following changes:

- (IBAction)chooseColor:(id)sender {
    NSInteger selectedColorIndex = [(UISegmentedControl *)sender selectedSegmentIndex];
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    [prefs setInteger:selectedColorIndex forKey:@"selectedColorIndex"];
    [prefs synchronize];
    NSUbiquitousKeyValueStore *prefs = [NSUbiquitousKeyValueStore defaultStore];
    [prefs setLongLong:selectedColorIndex forKey:@"selectedColorIndex"];
}

Here, we grab a slightly different object instead of NSUserDefaults. This new class doesn’t have a setInteger: method, so we use setLongLong: instead, which will do the same thing.

Next, find the viewDidAppear: method and change it as shown here:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
    NSInteger selectedColorIndex = [prefs integerForKey:@"selectedColorIndex"];
    NSUbiquitousKeyValueStore *prefs = [NSUbiquitousKeyValueStore defaultStore];
    NSInteger selectedColorIndex = (int)[prefs longLongForKey:
                                         @"selectedColorIndex"];
    [self setTintColorForIndex:selectedColorIndex];
    [self.colorControl setSelectedSegmentIndex:selectedColorIndex];
 
}

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 the next time a document is opened there. 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, or take a look at More iOS 7 Development: Further Explorations of the iOS SDK, by Kevin Kim, Alex Horowitz, Dave Mark, and Jeff LaMarche (Apress, 2014) for a hands-on guide to building an iCloud-backed Core Data app. We should point out that this architecture is a lot more complex and problematic than normal iCloud document storage. Apple has taken steps to improve things in iOS 7, 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.

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

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