Creating a Custom Document Object
A document-based application must have an instance of a subclass of UIDocument
that represents and manages document data. This chapter discusses the method overrides most applications need to make and offers suggestions for overriding other methods. For the core override points—the loadFromContents:ofType:error:
and contentsForType:error:
methods—examples are given for both NSData
and NSFileWrapper
as types of document data read from and written to a file. Storing Document Data in a File Package further explains how to use file-wrapper objects for document data.
You can override methods of UIDocument
other than the ones discussed in this chapter to read and write document data for particular purposes—for example, to write and read document data incrementally. However, these more advanced overrides have more complex requirements and should be avoided if possible. See UIDocument Class Reference for discussions of these overrides.
Declaring the Document Class Interface
In Xcode, add new Objective-C source and header files to your project, naming them appropriately (suggestion: work “Document” into the name). In the header file, change the superclass to UIDocument
and add properties to hold the document data. In Listing 3-1, the document data is plain text, so an NSString
property is all that is needed to hold it. (The text will be converted to an NSData
object that is written to the document file.)
Listing 3-1 Document subclass declarations (NSData
)
@interface MyDocument : UIDocument { |
} |
@property(nonatomic, strong) NSString *documentText; |
@end |
Listing 3-2 illustrates set of declarations for another application that uses an NSFileWrapper
object as the data-representation type. (The code examples in the chapter alternate between the two applications.) Not only is there a property to hold the file-wrapper object, there are properties to hold the text and image components of the represented file package.
Listing 3-2 Document subclass declarations (NSFileWrapper
)
@interface ImageNotesDocument : UIDocument |
@property (nonatomic, strong) NSString* text; |
@property (nonatomic, strong) UIImage* image; |
@property (nonatomic, strong) NSFileWrapper *fileWrapper; |
@property (nonatomic, weak) id <ImageNotesDocumentDelegate> delegate; |
@end |
@protocol ImageNotesDocumentDelegate <NSObject> |
-(void)noteDocumentContentsUpdated:(ImageNotesDocument*)noteDocument; |
@end |
This code shows additional declarations for a delegate and the protocol that it adopts. The document object’s view controller makes itself the delegate of the document object (and adopts the protocol) so that it can be notified (via noteDocumentContentsUpdated:
messages) of modifications to the document file. Listing 3-4 shows when and how the noteDocumentContentsUpdated:
message is sent.
Loading Document Data
When an application opens a document (at the user’s request), UIDocument
reads the contents of the document file and calls the loadFromContents:ofType:error:
method, passing in an object encapsulating the document data. That object can be an NSData
object or an NSFileWrapper
object. In your override of the method, initialize the document’s internal data structures (that is, its model objects) from the contents of the passed-in object.
The example in Listing 3-3 creates a string from the passed-in NSData
object and assigns it to the documentText
property. It also informs its delegate (in this case, the document’s view controller) of the updated document contents by invoking a protocol method. The motivation behind this delegation message is that the loadFromContents:ofType:error:
method is called not only as the result of opening a document, but also because of iCloud updates and reversion operations (revertToContentsOfURL:completionHandler:
).
Listing 3-3 Loading a document’s data (NSData
)
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError { |
if ([contents length] > 0) { |
self.documentText = [[NSString alloc] initWithData:(NSData *)contents encoding:NSUTF8StringEncoding]; |
} else { |
self.documentText = @""; |
} |
if ([_delegate respondsToSelector:@selector(noteDocumentContentsUpdated:)]) { |
[_delegate noteDocumentContentsUpdated:self]; |
} |
return YES; |
} |
If you have more than one document type, check the typeName
parameter; a different document type might affect how your code handles the document-data object. If your code experiences an error that prevents it from loading document data, return NO
; optionally, you can return by reference an NSError
object that describes the error.
The example in Listing 3-4 handles document data in the form of an NSFileWrapper
object. It simply assigns this object to its property.
Listing 3-4 Loading a document’s data (NSFileWrapper
)
-(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError { |
self.fileWrapper = (NSFileWrapper *)contents; |
if ([_delegate respondsToSelector:@selector(noteDocumentContentsUpdated:)]) { |
[_delegate noteDocumentContentsUpdated:self]; |
} |
return YES; |
} |
In this code, the method implementation does not extract the text and image components of the file wrapper and assign them to their properties. That is done lazily in the getter methods for the text
and image
properties.
Supplying a Snapshot of Document Data
When a document is closed or when it is automatically saved, UIDocument
sends the document object a contentsForType:error:
message. You must override this method to return a snapshot of the document’s data to UIDocument
, which then writes it to the document file. Listing 3-5 gives an example of returning a snapshot of document data in the form of an NSData
object.
Listing 3-5 Returning a snapshot of document data (NSData
)
- (id)contentsForType:(NSString *)typeName error:(NSError **)outError { |
if (!self.documentText) { |
self.documentText = @""; |
} |
NSData *docData = [self.documentText dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO]; |
return docData; |
} |
If the documentText
property has not yet been assigned any string value yet, it is assigned an empty string before it’s used to create an NSData
object.
Listing 3-6 shows an implementation of the same method that returns an NSFileWrapper
object. Basically, if a top-level (directory) file-wrapper object doesn’t exist, the code creates it; and if the two contained (regular file) file-wrapper objects do not exist, the code creates them from the values of the text
and image
properties. Then, it returns the top-level file wrapper to UIDocument
, which creates a file package in the file system. See Storing Document Data in a File Package for a more detailed explanation of file packages and documents.
Listing 3-6 Returning a snapshot of document data (NSFileWrapper
)
-(id)contentsForType:(NSString *)typeName error:(NSError **)outError { |
if (self.fileWrapper == nil) { |
self.fileWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil]; |
} |
NSDictionary *fileWrappers = [self.fileWrapper fileWrappers]; |
if (([fileWrappers objectForKey:TextFileName] == nil) && (self.text != nil)) { |
NSData *textData = [self.text dataUsingEncoding:TextFileEncoding]; |
NSFileWrapper *textFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:textData]; |
[textFileWrapper setPreferredFilename:TextFileName]; |
[self.fileWrapper addFileWrapper:textFileWrapper]; |
} |
if (([fileWrappers objectForKey:ImageFileName] == nil) && (self.image != nil)) { |
@autoreleasepool { |
NSData *imageData = UIImagePNGRepresentation(self.image); |
NSFileWrapper *imageFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:imageData]; |
[imageFileWrapper setPreferredFilename:ImageFileName]; |
[self.fileWrapper addFileWrapper:imageFileWrapper]; |
} |
} |
return self.fileWrapper; |
} |
Storing Document Data in a File Package
A file package has an internal structure that is reflected in the methods of the NSFileWrapper
class. A file wrapper is a runtime representation of a file-system node, which is either a directory, a regular file, or a symbolic link. As shown in Figure 3-1, a file package is a file-system node, typically a directory and its contents, that the operating system treats as a single, opaque entity. It is similar in concept to a bundle.
You programmatically compose a file package by creating a top-level directory file wrapper and then adding to that container regular files and subdirectories, each represented by other NSFileWrapper
objects. File wrappers inside the top-level directory should have preferred names associated with them.
With this brief overview in mind, look again at the following lines of code from the contentsForType:error:
method in Listing 3-6. The file package created in this method has two components, a text file and an image file. (The creation of the image file wrapper is not shown in the snippet.)
if (self.fileWrapper == nil) { |
self.fileWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil]; |
} |
NSDictionary *fileWrappers = [self.fileWrapper fileWrappers]; |
if (([fileWrappers objectForKey:TextFileName] == nil) && (self.text != nil)) { |
NSData *textData = [self.text dataUsingEncoding:TextFileEncoding]; |
NSFileWrapper *textFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:textData]; |
[textFileWrapper setPreferredFilename:TextFileName]; |
[self.fileWrapper addFileWrapper:textFileWrapper]; |
} |
The code creates a top-level directory if it doesn’t exist. If a file wrapper doesn’t exist for the text file, it creates one from the string contents of the text property. It gives this file wrapper a preferred filename and then adds it to the top-level directory file wrapper.
For more on NSFileWrapper
, see NSFileWrapper Class Reference; also see Exporting the Document UTI for the required Info.plist
property for document file packages.
Other Method Overrides You Might Make
There are a few other UIDocument
overrides that many document-based applications might want to make:
disableEditing
...enableEditing
—UIDocument
calls the first method when it is unsafe for the user to make changes to document content, such as when there are updates from iCloud or a revert operation is underway. You can implement this method to prevent editing during this period. When editing becomes safe again,UIDocument
calls the second method.savingFileType
—This method by default returns the value of thefileType
property. If the current document should be saved under a different file type for any reason, you can override this method to return the replacement file-type UTI. An example (from Mac OS X) is that when an image is added to an RTF file, it should be saved as an RTFD file package.
Copyright © 2012 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2012-09-19