It contains nothing when first launched, as shown in Figure 19-1. Enter the detailsfor a couple of items.
Figure 19-1. Testing document storage
Now either wait about 20 seconds or press the home button to push the app into the background state.
When you created a new item, the document was notified of the change. The auto-save feature of UIDocument periodicallysaves the document when the user isn’t doing anything else, and will immediately save it when your app is moved to the background state.
With your data safely saved in the document, stop the app and run it again from Xcode. You should be
rewarded for all of your hard work with the list of items you entered earlier.
What you see, however, is an empty screen, shown on the right in Figure 19-1.
So what went wrong? Maybe your document isn’t being opened when your app starts? Maybe it didn’t get saved in the first place? What you do know is that you’ve got a bug; it’s time to turn to the debugger.
Setting Breakpoints
Switch back to Xcode and set a breakpoint in your -contentsForType:error: by clicking in the gutter to the left of the code, as shown in Figure 19-2. A breakpoint appears as a blue tab.
Figure 19-2. Setting a breakpoint in -contentsForType:error:
Uninstall your My Stuff app on your device or simulator. (Tap and hold the My Stuff app icon in the
springboard until it starts to shake, tap the delete (x) button, agree to delete the app, and press the home button again.) This deletes your app and any data, including any documents, stored on the device. Run the app again. Xcode will reinstall the app and it will run with a fresh start.
Almost immediately, Xcode stops at the breakpoint in the -contentsForType:error: method, as shown in Figure 19-2. If you look at the stack trace on the left, you can see that the
-contentsForType:error: message was sent from the +documentAtURL: method, which was sent from -viewDidLoad. Together, this tells you that -contentsForType:error: is being sent to create the initial, empty, document when nodocument exists.
Stepping Through Code and Examining Variables
Another way to verify this is to examine the value of the things array when the method executes.
The things object is your data model. When the method is entered for the first time, you can see
that the things variable is nil. You can examine its value either in the debugging pane (at the bottom of the window) or by hovering your cursor over a variable name, as shown in Figure 19-3.
Figure 19-3. Stepping through -contentsForType:error:
Click on the Step Over button (See Figure 19-2) to execute one line of code. Continue stepping over lines, watching as the code compares the things variable to nil and then creates an empty array (replacing nil with "0 objects"),as shown in Figure 19-3.
To let your app run at full speed again, click the Continue button, just to the left of the Step Over button.
Your app will resume full speed execution until it encounters another breakpoint. Back in your app, add an item or twoand then pause. The auto-save mechanism will eventually kick in and
send another -contentForType:error: message, and your app will again stop at the breakpoint.
This time, the things array contains new MyWhatsit objects, as shown in Figure 19-4.
Figure 19-4. Contents of things array during second save
This confirms that your app is adding MyWhatsit objects to the things array, serializing it, and returning it to UIDocument for saving. The problem isn’t that the document isn’t being saved. So there must be a problem loading thedocument. By the way, this is called the “divide and conquer” debugging technique. Decide what your
code should be doing, set a breakpoint somewhere in the middle of that process, and see if that step is
happening correctly. If not,the problem is either right there or earlier in your code. If it is happening correctly,
the problem is after that point. Choose another breakpoint and repeat until you’ve found the bug.
Run the app again, but first set a breakpoint in your -loadFromContents:ofType:error: method. When your app starts, you’ll see that the -loadFromContents:ofType:error: method is received immediately, as shown in Figure 19-5.
Figure 19-5. Checking to see that -loadFromContents:ofType:error: is received
Using the step over command, you can watch the code obtain the wrapper object, extract its data, and turn that data back into the things array. As you can see in Figure 19-5, all of your data has been restored.
So your document is being opened, its contents read, and yet the table view is still empty. What insanity is this? To make matters worse, if you try to add a new item, your app crashes. Why is this happening
(to you)?
As it turns out, the problem isn’t that mysterious. If you set a breakpoint in the +documentAtURL: method and also in MSMasterViewController’s -tableView:numberOfRowsInSection:
method (one of the first messages the tableview data source receives),
as shown in Figure 19-6, you’ll discover two things. First, -tableView:numberOfRowsInSection: is received after +documentAtURL:, but before the -loadFromContents:ofType:error: message is received. Secondly, if you examine the document object (also shown in Figure 19-6), you’ll see that the things array is still nil.
Figure 19-6. Examining the document object in the debugger
Have you figured it out yet? UIDocument’s openWithCompletionHandler: method (sent in+documentAtURL:) is
asynchronous. It starts the process of retrieving your document’s data in the background and returns
immediately. Your app’s code proceeds, displaying the table view, with a still empty data model.
Some time later, the data for the document finishes loading and is passed to loadFromContents:ofType:error: to
be converted into a data model. That’s successful, but the table view doesn’t know that and continues to
display—what it thinks is—an empty list.
What your document needs to do is notify your view controller when the data model has been updated, so thetable view can refresh itself. You could accomplish this using a notification or a code block property, but I
think themost sensible solution is to use a delegate message. As a bonus, you’ll get practice creating your own protocol.
Define a new delegate protocol. You could add a new header file to the project just for this protocol, but since it goes hand-in-hand with the MSThingsDocument class, I recommend adding it to the end of the MSThingsDocument.hinterface file:
@protocol MSThingsDocumentDelegate <NSObject>
@optional
- (void)gotThings:(MSThingsDocument*)document;
@end
This defines a protocol with one, optional, method (-gotThings:), sent whenever your document object loads new things from the document. Back up to the beginning of the MSThingsDocument.h file, find the @class MyWhatsitstatement, and add a forward declaration for the new protocol and a delegate property (new code in bold):
@class MyWhatsit
@protocol MSThingsDocumentDelegate;
@interface MSThingsDocument : UIDocument
+ (NSURL*)documentURL;
+ (MSThingsDocument*)documentAtURL:(NSURL*)url;
@property (weak) id<MSThingsDocumentDelegate> delegate;
Switch to the MSThingsDocument.m implementation file. In the +documentAtURL: method, change the statement that opens the document to this (modified code in bold):
[document openWithCompletionHandler:^(BOOL success){ if (success)
{
if ([document.delegate respondsToSelector:@selector(gotThings:)]) [document.delegate gotThings:document];
}
}];
The modified code now performs an action after the document is finished loading, which includes the unarchiving of the data model objects. Now it sends its delegate a -gotThings: message, so the delegate (your view controller) knows that the data model has changed.
Switch to the MSMasterViewController.h file and make your view controller a document delegate (new code in bold):
@interface MSMasterViewController
: UITableViewController <MSThingsDocumentDelegate>
Over in the MSMasterViewController.m implementation file, make two changes. Immediately after obtaining the new document object in -viewDidLoad, make the view controller the document’s delegate object (new code in bold):
document = [MSThingsDocument documentAtURL:[MSThingsDocument documentURL]];
document.delegate = self;
Finally, add the optional -gotThings: method to the @implementation section:
- (void)gotThings:(MSThingsDocument *)document
{
[self.tableView reloadData];
}
Run your app again, as shown in Figure 19-7, and voilà! The data in your document appears in the table view.
Figure 19-7. Working document
Make changes or add new items. Press the home button to give UIDocument a chance to save the document, stop the app, restart it, and your changes persist. The only content MyStuff doesn’t save is any images you add.That’s because images aren’t part of the archived object data. You’re going to add image data directly to the document’s directory wrapper, so attack that problem next.
Storing Image Files
Image data storage takes a different route than the other properties in your MyWhatsit objects. Here is how it’s going to work:
n When a new, or updated, image (UIImage) object is added to a MyWhatsit object, the image is converted into the PNG (Portable Network Graphics) data format and stored in
the document as a file wrapper.The MyWhatsit object remembers the key of the file wrapper.
n When the document is saved, UIDocument automatically includes the data from all the file wrappers in the document. The image file wrapper keys are archived by the MyWhatsit objects.
n When the document is opened again, the file wrapper objects for the image data are restored.
n When client code requests the image property of a MyWhatsit object, MyWhatsit uses its saved key
to locate and load the dat a in the file wrapper, eventuallyconverting it back into the original UIImage object.
The key to this design (no pun intended) is the relationship between the MyWhatsit objects and the documentobject. A MyWhatsit object will use the document object to store, and later retrieve, the data for an individual
image.From a software design standpoint, however, you want to keep the code that actually stores and retrievesthe image data out of the MyWhatsit object. The single responsibility principle encourages the MyWhatsit objectto do what itdoes (represent the values in your data model) and the document object to do what it does
(manage the storage and conversion of document data) without polluting one class with the responsibilities of
the other.
The solution is to create an abstraction layer, or abstract service, in the MSThingsDocument class to store and retrieve
images. MyWhatsit will still instigate image management, but the mechanics of how those images getturned into file wrappers stays inside MSThingsDocument. Let’s get started.
Add two public methods to the @interface in MSThingsDocument.h:
- (NSString*)setImage:(UIImage*)image existingKey:(NSString*)key;
- (UIImage*)imageForKey:(NSString*)key;
The first method will store, or replace, an image in the document. The second will retrieve one.
Now modify MyWhatsit to use these methods to save and restore its image property.
Select the MyWhatsit.h interface file. Add a forward reference to the MSThingsDocument class, a new document property, and a readonly imageKey property (new code in bold):
@class MSThingsDocument;
@interface MyWhatsit : NSObject <NSCoding>
@property (weak,nonatomic) MSThingsDocument *document;
@property (readonly,nonatomic) NSString *imageKey;
While you’re here, delete the -initWithName:location: method you originally added to help with testing in Chapter 5. You’re not using it anymore.
The document property contains a reference to the document where this object stores and retrieves its image. The imageKey property is the key of the file wrapper that contains this object’s image data. Modify your image handling to use these new properties.
Select the MyWhatsit.m implementation file. Begin by importing the document object interface, just below the other #import directives:
#import "MSThingsDocument.h"
Before the @implementation section, add one more archiving key and a private interface section that defines two instance variables, one for the image and one for the image data key:
#define kImageKeyCoderKey @"image.key"
@interface MyWhatsit ()
{
UIImage *image; NSString *imageKey;}
Delete the -initWithName:location: method you are no longer using.
In the -encodeWithCoder: method, add a statement to archive the value of the imageKey property:
[coder encodeObject:imageKey forKey:kImageKeyCoderKey];.
To the -initWithCoder: method, add a matching statement, immediately after the other
-decodeObjectForKey: messages, to restore it when it’s unarchived:
imageKey = [decoder decodeObjectForKey:kImageKeyCoderKey];
You don’t add the actual image data to the archive, but your object does need to remember the key to where that data is stored in the document’s directory wrapper.
Now you can define a custom getter and setting method for the image property. Start with the getter:
- (UIImage*)image
{
if (image==nil && imageKey!=nil)
image = [_document imageForKey:imageKey]; return image;
}
The image property getter method now checks for the situation where it does not have an image object (image==nil), but it does have a key for an image stored in the document (imageKey!=nil). In that case, it retrieves the image object stored in the document. This is done lazily; that is, the first time the image property is requested. When the table view first appears, only those items visible in the list will load their images. The rest of the items in thedocument won’t be loaded until the user scrolls the list to reveal them.
The image property setter method has to keep the document up to date. After the getter, add its companion setter method:
- (void)setImage:(UIImage *)newImage
{
imageKey = [_document setImage:newImage existingKey:imageKey]; image = newImage;
}
The setter method either adds, or replaces, the image in the document. The document’s
-setImage:existingKey: method stores the image data in a file wrapper and returns the key identifying that wrapper. The existingKey parameter passes in the key of the image data the object had previously stored. This is used bythe document to delete any old image data before adding the new. Finally, the image object is retained.
Finally, a getter method for the imageKey property must be supplied:
- (NSString*)imageKey
{
return imageKey;
}
That concludes all of the changes to the MyWhatsit class. Select the MSThingsDocument.m implementation file. Obviously, you need to supply the two image storage methods you defined in the interface. Start with the -setImage:existingKey: method:
- (NSString*)setImage:(UIImage *)image existingKey:(NSString *)key
{
if (key!=nil)
{
NSFileWrapper *imageWrapper = docWrapper.fileWrappers[key]; if (imageWrapper!=nil)
[docWrapper removeFileWrapper:imageWrapper];
}
NSString *newKey = nil; if (image!=nil)
{
NSData *imageData = UIImagePNGRepresentation(image); newKey = [docWrapper addRegularFileWithContents:imageData
preferredFilename:kImagePreferredName];
}
[self updateChangeCount:UIDocumentChangeDone]; return newKey;
}
It works just as you would expect it to. If the sender included a key for an existing file wrapper, that file wrapper is first removed. If an image is being stored (image!=nil), the image is encoding into the PNG file format
by the UIImagePNGRepresentation function. The compressed image data is then added to the directory
package as a new file wrapper, and the key that identifies that wrapper is returned to the sender. Of course, you didn’t forget to tell the document that its content has changed before returning.
That takes care of storing a new image in the document and replacing an existing image with a new one. Now add the code to retrieve images from the document:
- (UIImage*)imageForKey:(NSString *)key
{
UIImage *image = nil; if (key!=nil)
{
NSFileWrapper *imageWrapper = docWrapper.fileWrappers[key]; if (imageWrapper!=nil)
image = [UIImage imageWithData:imageWrapper.regularFileContents];
}
return image;
}
This method performs the inverse of the -setImage:existingKey: method. It uses key to find the file wrapper in the document, sends the wrapper a -regularFileContents message to retrieve the data, and uses that PNG image data toreconstruct the original UIImage object, which is returned to the sender.
Sneakily, there’s one more place where an image is removed from the document: when the user deletes a MyWhatsit object. Locate the -removeWhatsitAtIndex: method.
Add code to the beginning of the method to remove theimage file wrapper for that item, before removing that item (new code in bold):
- (void)removeWhatsitAtIndex:(NSUInteger)index
{
MyWhatsit *thing = things[index]; if (thing.imageKey!=nil)
[self setImage:nil existingKey:thing.imageKey];
[things removeObjectAtIndex:index];
[self updateChangeCount:UIDocumentChangeDone];
}
All of the mechanics for saving, retrieving, and deleting images from the document are in place. Sadly, none of it will work. The MyWhatsit must be connected to the working MSThingsDocument object through its documentproperty for any of this new code to function. At this point, no one is setting that property.
So where should the document property be set, and what object should be responsible for setting it? The answer is the MSThingsDocument object. It should take responsibility for maintaining the connection between itself andits data model objects.
As it turns out, this is an incredibly easy problem to solve, because there are only two locations where MyWhatsit objects are created: when the document is unarchived and when the user creates a new item. Start with the -anotherWhatsit method and add a statement to set the new object’s document property (new code in bold):
- (MyWhatsit*)anotherWhatsit
{
MyWhatsit *newItem = [MyWhatsit new];
newItem.document = self;
Locate the -loadFromContents:ofType:error: method. Immediately after the things array is unarchived, add this statement (new code in bold):
if (thingsData!=nil)
{
things = [NSKeyedUnarchiver unarchiveObjectWithData:thingsData];
[things makeObjectsPerformSelector:@selector(setDocument:) withObject:self];
}
The -makeObjectsPerformSelector:withObject: message sends a message with one parameter to every object in the collection (equivalent to [things[0] setDocument:self], [things[1] setDocument:self], andso on). When it’s done,every MyWhatsit object in the array will have its document property set with the document where its image is stored.
Your document implementation is finally finished! Give it a spin by running MyStuff.
Add some items, attach some pictures, and quit the app, as shown in Figure 19-8. Stop the app in Xcode and start it again. All of the items,along with their pictures, are preserved in the document.
Figure 19-8. Testing image storage
You’ll fix the former in Chapter 24, and you can take a stab at the orientation problem in the exercise at the end of thischapter.
If you’re running MyStuff on a provisioned device, you can see your app’s document file(s) in the Devices tab of
the Organizer window (Window ➤ Organizer). Select the Devices tab, select the Applications installed on your device, and then select the MyStuff app. The organizer window will show you the files in your app’s sandbox, as shown in Figure 19-9.
Figure 19-9. MyStuff sandbox files
You can clearly see your Things I Own.mystuff document package inside your app’s Documents folder. The funny filenames (1 #$!@%!# image.png) is how UIDocument handles two or more file wrappers with the samepreferred name. It gives the files crazy names so they can all be stored in the same directory.
If you need to get these files, use the Download button at the bottom of the window. Xcode will copy the files from your iOS device and save them on your hard drive, where you can examine them.
Odds and Ends
What you’ve accomplished in MyStuff is a lot, but it really represents the bare minimum of support required for document integration. There are lots of features and issues that I skipped over. Let’s review a couple of
those now.
iCloud Storage
You can store your documents in the cloud, much as your stored property list values in the cloud in Chapter 18. Documents, naturally, are a little more complicated.
Apple’s guidelines suggest that you provide a setting that allows the user to place all of their documents in the cloud or none oftheir documents in the cloud. A piecemeal approach is not recommended. When the userchanges their mind, your app is responsible for moving (copying) the locally stored documents into the cloud or in the other direction. This isn’t a trivial task. It involves multi-tasking, which I don’t get to until Chapter 24.
Once in the cloud, you open, modify, and save documents much the way you do from your local sandbox. All of the code you wrote for -contentsForType:error: and
-loadFromContents:ofType:error: won’t need any modification (if you wrote them correctly), you’ll
just use different URLs. In reality, the data of your “cloud” documents are actually stored locally on the
device. Any changes are synchronized with the iCloud storage servers in the background, but you always
retain a localcopy of the data, both for speed and in case the network connection with the cloud is
interrupted.
There are some subtle, and not so subtle, differences between local and cloud-based documents.
One of the big differences is change. Changes to your cloud documents can occur at any time. The user is free to edit the samedocument on another device, and network interruptions can delay those changes from reaching your app immediately.
In general, your app observes the UIDocumentStateChangedNotification notification. If the iCloud service detects conflicting versions (local vs. what’s on the server), your document’s state will change to UIDocumentStateInConflict.It’s then up to your app to compare the two documents and decide what to keep and what to discard. You might query the user for guidance, or your app might do it automatically.
To learn more about iCloud documents, start with the Document-Based App Programming Guide for iOS, that you can find in Xcode’s Documentation and API Reference window. It’s a good read, and I strongly suggest youperuse it if you plan to do any more development involving UIDocument.
The chapters “Managing the Life Cycle of a Document” and “Resolving Document Version Conflicts”
directly address cloud storage.
Archive Versioning
When implementing NSCoding, you might need to consider what happens when your class changes. One of the consequences of archiving objects to persistent storage is that the data is—well— persistent. Users will expectyour app to open documents created years ago. I’m trying to improve my software all of the time, and I assume you are too. I’m always adding new properties to classes, or changing the type and scope of properties. It oftenmeans creating new classes and periodically abandoning old ones. All such changes alter the way classes encode themselves and pose challenges when unarchiving data created by older, and sometimes newer, versions of your software
There are a number of techniques for dealing with archive compatibility. Your newer code might encode its values using a different key. When decoding, your software can test for the presence of that key to determine if thearchive data was created by modern or legacy software. You might encode a “version” value in your archive data and test that version when decoding. Newer software might encode a value in both its modern form and a legacy form, so that older software (that knows nothing of the newer form)can still interpret the document data.
There are even techniques for substituting one class for another during unarchiving. This can solve the problem of an encoded class that no longer exists. A thorough discussion of these issues, and some solutions, arediscussed in the “Forward and Backward Compatibility for Keyed Archives” chapter of the Archives and Serializations Programming Guide.
Summary
Embracing UIDocument adds a level of modern data storage to your app that users both appreciate and have come to expect. You’ve learned how, and where, to store your app’s documents. More importantly,
you understand thedifferent roles that objects and methods play that, together, orchestrate the transformation of model objects into raw data, and back again. You learned how to construct multi-file documents that can be incrementally saved andlazily loaded. Along the way, you learned how to archive objects and create objects that can be archived.
You’ve come a long way, and you should be feeling pretty confident in your iOS aptitude. Adding persistent
storage to your apps was really the last major iOS competency you had to accomplish. The next couple of
chapters diginto Objective-C, to hone your language knowledge and proficiency.









No comments:
Post a Comment