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.

Figure 3-1  Structure of a file package

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: