- Setting Up an iCloud-Compatible Project
- How iCloud Works
- iCloud Container Folders
- Recipe: Trying Out iCloud
- Working with UIDocument
- Recipe: Subclassing UIDocument
- Metadata Queries and the Cloud
- Handy Routines
- Recipe: Accessing the Ubiquitous Key-Value Store
- Recipe: UIManagedDocument and Core Data
- Summary
Working with UIDocument
The UIDocument class is an abstract class that supports cloud-ready asynchronous reading and writing of data. UIDocument simplifies the way you work with iCloud data, because it handles nearly all the issues you encounter about coordinating local data with cloud data. Recipe 18-1’s “just write to the right folder” approach cannot be recommended for serious development. What UIDocument does is add a shell for managing your document, letting you send read, save, and write requests through it in a safer and more structured way.
You never work directly with the UIDocument class. Instead, you subclass it and implement several simple key features that link the class to the ubiquitous data.
Establishing the Document as a File Presenter
UIDocument implements the NSFilePresenter class. This means that you can register the class to receive updates whenever its cloud data gets updated by telling NSFileCoordinator to add the document as “presenter.” This is terribly unhelpful naming, I know, but what it means is that a presenter class is one that takes a strong interest in knowing when outside changes happen to a given file. Receiving alerts about these changes allows it to update its GUI presentation in response.
The registration process works like this. You create a document and a coordinator, initializing the coordinator with the document as its performer as the following code snippet shows. Registering the new document/presenter with NSFileCoordinator class allows it to receive updates. You must perform both these steps, both at the instance and class level.
imageDocument = [[ImageDocument alloc] initWithFileURL:ubiquityURL]; imageDocument.delegate = self; coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:imageDocument]; [NSFileCoordinator addFilePresenter:imageDocument];
Although all the actual coordination implementation details are handled for you via the UIDocument class (for example, UIDocument handles all file coordination issues for you, so you do not need to manually coordinate reading and writing), this set up portion is not. Unless you establish the presenter/coordinator pair manually, your document will not receive updates from the cloud.
Moving to the Cloud
NSFileManager allows you to move local files to and from the cloud using the setUbiquitous:itemAtURL:destinationURL:error: method. What this method does is nothing more than move (not copy) the file in question from your sandbox into the central iCloud folder and back. However, it does so safely in an approved manner.
The method takes three arguments. The first establishes the direction of movement. YES brings items from the sandbox to the cloud and NO the reverse. The second argument must always be the source URL and the third its destination. The first argument is a little pointless in that the source URL will always be either sandbox or cloud and the destination must be the reverse. If all three arguments do not line up, the method will fail.
To move to the cloud, use YES, the local sandbox URL, and the destination cloud URL, as shown here. To remove an item from the cloud, use NO, then the cloud URL, and then the local sandbox URL.
if (![[NSFileManager defaultManager] setUbiquitous:YES itemAtURL:localURL destinationURL:ubiquityURL error:&error]) { NSLog(@"Error making local file ubiquitous. %@", [error localizedFailureReason]); return; }
Retrieve the cloud URL by appending the file name to the results of the ubiquityDocumentsURL method, which is shown in Recipe 18-1.
Evicting Items from the Cloud
Removing an item from the cloud by setting its ubiquity to NO deletes it from both the cloud and from all of your user’s devices and computers. At times, you may want to remove a local copy in order to force it to refresh from the cloud without deleting it. To do this, do not alter the file’s ubiquity. Instead, call an NSFileManager eviction method, as follows. Evicting a file allows iCloud to retrieve a new copy, but does not remove it from central cloud storage.
+ (BOOL) evictFile: (NSString *) filename forContainer: (NSString *) container { if (!filename) return NO; NSURL *targetURL = [self ubiquityDocumentsFileURL:filename forContainer:container]; BOOL targetExists = [[NSFileManager defaultManager] fileExistsAtPath:targetURL.path]; if (!targetExists) return NO; NSError *error; if (![[NSFileManager defaultManager] evictUbiquitousItemAtURL:targetURL error:&error]) { NSLog(@"Error evicting current copy of %@.: %@", filename, error.localizedFailureReason); return NO; } return YES; } + (BOOL) evictFile: (NSString *) filename { return [self evictFile:filename forContainer:nil]; }
In addition to file eviction, you can actually force iCloud to download a new copy. This is of use only when you write your own data handlers. You pretty much never have to do this if you use Apple’s recommended UIDocument and UIManagedDocument classes.
+ (BOOL) forceDownload: (NSString *) filename forContainer: (NSString*) container { if (!filename) return NO; NSURL *targetURL = [self ubiquityDocumentsFileURL:filename forContainer:container]; if (!targetURL) return NO; NSError *error; if (![self evictFile:filename forContainer:container]) return NO; if (![[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:targetURL error:&error]) { NSLog(@"Error starting download of %@: %@", filename, error.localizedFailureReason); return NO; } return YES; } + (BOOL) forceDownload: (NSString *) filename { return [self forceDownload:filename forContainer:nil]; }
Opening and Saving Files
You open and save UIDocument instances to read them in from a file and to store your changes out. Opening the file is generally performed as part of creating the document. You provide it with a file URL pointing to the stored file, and ask it to open the file. For iCloud materials, this path must point to the correct place in the ubiquitous documents folder to work with iCloud and not to a file saved locally. You can also use the UIDocument class to access and read non-iCloud files that have been moved back to the sandbox. This flexibility unifies how you access files, letting users choose which items to send to the cloud.
imageDocument = [[ImageDocument alloc] initWithFileURL:theURL]; [imageDocument openWithCompletionHandler:^(BOOL success) { NSLog(@"Open file was: %@", success ? @"successful" : @"failure"); if (success) imageView.image = imageDocument.image;}];
Each time you make changes to your document, you’ll want to save those updates as transparently as possible. Saving data, especially to small files, is cheap. Making updates as soon as your user creates changes allows you to build a persistent system that synchronizes with your user’s adjustments.
imageDocument.image = image; imageDocument saveToURL:imageDocument.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success){ NSLog(@"Attempt to save to URL %@", success ? @"succeeded" : @"failed"); }];
UIDocument is fully undo-aware and provides its own undoManager property hook. This allows you to save without having to worry about creating permanent changes from which you cannot recover.
Subscribing to UIDocument Notifications
When working with documents, your primary class must subscribe to a single notification: UIDocumentStateChangedNotification. Your handler uses that notification to handle conflict issues, allowing your document to update to the latest version. At its simplest, the notification subscription looks like this. This snippet resolves all conflict issues by accepting the most recent version and updating its presentation. This example, which works with the code in Recipe 18-2, is built around a simple image presentation app. When new images arrive, the application view updates and shows them.
[[NSNotificationCenter defaultCenter] addObserverForName: UIDocumentStateChangedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification __strong *notification) { if (imageDocument.documentState == UIDocumentStateInConflict) { NSError *error; NSLog(@"Resolving conflict. (Newest wins.)"); if (![NSFileVersion removeOtherVersionsOfItemAtURL: imageDocument.fileURL error:&error]) { NSLog(@"Error removing other document versions: %@", error.localizedFailureReason); return; } imageView.image = imageDocument.image; } }];
This code, which uses “newest version wins,” represents the simplest way you can handle document conflicts using NSFileVersion, a class built around time-based file revisions. Although it’s the simplest approach, it’s not always the best. You’ll probably want to use a little more finesse in your applications. iOS can store more than one version of a file at a time. You can retrieve all conflicted versions of the file version class through something like the following. The snippet
[NSFileVersion unresolvedConflictVersionsOfItemAtURL: imageDocument.fileURL]
returns an array of file version instances. You can iterate through those instances to help resolve your conflicts, choosing which of the conflicted saves best represents the state that you want to present to the user.
When you need to examine the actual version data to better judge or merge that material, you can pull each file’s versioned contents from the instance’s URL property. From there, you can select which file version to use by removing the other versions (as in the previous example) or replace the file data entirely.
The document’s documentState property lets you know whether your saves are currently conflicted or if you’re clear to make changes. Additional states include UIDocumentStateNormal, which allows users to make unhindered changes; UIDocumentStateClosed, indicating that the document wasn’t opened open properly (or has been deliberately closed) and is probably not valid for edits; UIDocumentStateSavingError, telling you that the file could not be saved out; and UIDocumentStateEditingDisabled, which means that the file is busy. A busy file is never safe for user edits, so your application should disable changes until the state changes back to something safer.
You may allow edits during both normal and conflict states but you should inform your user when conflict issues may prevent updates from propagating out to the cloud, such as the Mom-on-a-Plane scenario discussed earlier. Apple recommends offering visual feedback, such as a green light for unhindered updates and a yellow one for modifications that may need later intervention to integrate.