Creating the Subclass of NSDocument

The NSDocument subclass provides storage for the model and the ability to load and save document data. It also has any outlets and actions required for the user interface. The NSDocument object automatically creates an NSWindowController object to manage that nib file, but the NSDocument object serves as the File’s Owner proxy object for the nib file.

When you subclass NSDocument, you must override certain key methods and implement others to do at least the following things:

In particular, you must override one reading and one writing method. In the simplest case, you can override the data-based reading and writing methods, readFromData:ofType:error: and dataOfType:error:.

Reading Document Data

Opening existing documents stored in files is one of the most common operations document-based apps perform. Your override’s responsibility is to load the file data into your app’s data model.

If it works for your application, you should override the data-based reading method, readFromData:ofType:error:. Overriding that method makes your work easier because it uses the default document-reading infrastructure provided by NSDocument, which can handle multiple cases on your behalf.

How to Override the Data-Based Reading Method

You can override the readFromData:ofType:error: method to convert an NSData object containing document data into the document’s internal data structures and display that data in a document window. The document architecture calls readFromData:ofType:error:, passing in the NSData object, during its document initialization process.

Listing 4-1 shows an example implementation of the readFromData:ofType:error: document-reading method. This example assumes that the app has an NSTextView object configured with an NSTextStorage object to hold the text view’s data. The NSDocument object has a setMString: accessor method for the document’s NSAttributedString data model, declared as a property named mString.

Listing 4-1  Data-based document-reading method implementation

- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName
                                     error:(NSError **)outError {
    BOOL readSuccess = NO;
    NSAttributedString *fileContents = [[NSAttributedString alloc]
            initWithData:data options:NULL documentAttributes:NULL
            error:outError];
    if (!fileContents && outError) {
        *outError = [NSError errorWithDomain:NSCocoaErrorDomain
                                code:NSFileReadUnknownError userInfo:nil];
    }
    if (fileContents) {
        readSuccess = YES;
        [self setMString:fileContents];
    }
    return readSuccess;
}

If you need to deal with the location of the file, override the URL reading and writing methods instead. If your app needs to manipulate document files that are file packages, override the file-wrapper reading and writing methods instead. For information about overriding the URL-based and file-wrapper-based reading methods, see Overriding the URL and File Package Reading Methods.

The flow of messages during document data reading is shown in Figure 6-5.

It’s Easy to Support Concurrent Document Opening

A class method of NSDocument, canConcurrentlyReadDocumentsOfType:, enables your NSDocument subclass to load documents concurrently, using background threads. This override allows concurrent reading of multiple documents and also allows the app to be responsive while reading a large document. You can override canConcurrentlyReadDocumentsOfType: to return YES to enable this capability. When you do, initWithContentsOfURL:ofType:error: executes on a background thread when opening files via the Open dialog or from the Finder.

The default implementation of this method returns NO. A subclass override should return YES only for document types whose reading code can be safely executed concurrently on non-main threads. If a document type relies on shared state information, you should return NO for that type.

Don’t Rely on Document-Property Getters in Overrides of Reading Methods

Don’t invoke fileURL, fileType, or fileModificationDate from within your overrides. During reading, which typically happens during object initialization, there is no guarantee that NSDocument properties like the file’s location or type have been set yet. Your overridden method should be able to determine everything it needs to do the reading from the passed-in parameters. During writing, your document may be asked to write its contents to a different location or using a different file type.

If your override cannot determine all of the information it needs from the passed-in parameters, consider overriding another method. For example, if you see the need to invoke fileURL from within an override of readFromData:ofType:error:, you should instead override readFromURL:ofType:error: and use the passed-in URL value.

Writing Document Data

In addition to implementing a document-reading method, you must implement a document-writing method to save your document data to disk. In the simplest case, you can override the data-based writing method, dataOfType:error:. If it works for your application, you should override dataOfType:error:. Overriding that method makes your work easier because it uses the default document-reading infrastructure provided by NSDocument. The responsibility of your override of the dataOfType:error: method is to create and return document data of a supported type, packaged as an NSData object, in preparation for writing that data to a file.

Listing 4-2 shows an example implementation of dataOfType:error:. As with the corresponding example implementation document-reading method, this example assumes that the app has an NSTextView object configured with an NSTextStorage object to hold the document’s data. The document object has an outlet property connected to the NSTextView object and named textView. The document object also has synthesized mString and setMString: accessors for the document’s NSAttributedString data model, declared as a property named mString.

Listing 4-2  Data-based document-writing method implementation

- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError {
    NSData *data;
    [self setMString:[self.textView textStorage]]; // Synchronize data model with the text storage
    NSMutableDictionary *dict = [NSDictionary dictionaryWithObject:NSRTFTextDocumentType
                                                            forKey:NSDocumentTypeDocumentAttribute];
    [self.textView breakUndoCoalescing];
    data = [self.mString dataFromRange:NSMakeRange(0, [self.mString length])
                    documentAttributes:dict error:outError];
    if (!data && outError) {
        *outError = [NSError errorWithDomain:NSCocoaErrorDomain
                                code:NSFileWriteUnknownError userInfo:nil];
    }
    return data;
}

The override sends the NSTextView object a breakUndoCoalescing message when saving the text view’s contents to preserve proper tracking of unsaved changes and the document’s dirty state.

If your app needs access to document files, you can override writeToURL:ofType:error: instead. If your document data is stored in file packages, you can override fileWrapperOfType:error: instead. For information about overriding the other NSDocument writing methods, see Overriding the URL and File Package Writing Methods.

The actual flow of messages during this sequence of events is shown in detail in Figure 6-6.

Initializing a New Document

The init method of NSDocument is the designated initializer, and it is invoked by the other initializers initWithType:error: and initWithContentsOfURL:ofType:error:. If you perform initializations that must be done when creating new documents but not when opening existing documents, override initWithType:error:. If you have any initializations that apply only to documents that are opened, override initWithContentsOfURL:ofType:error:. If you have general initializations, override init. In all three cases, be sure to invoke the superclass implementation as the first action.

If you override init, make sure that your override never returns nil. Returning nil could cause a crash (in some versions of AppKit) or present a less than useful error message. If, for example, you want to prevent the creation or opening of documents under circumstances unique to your app, override a specific NSDocumentController method instead. That is, you should control this behavior directly in your app-level logic (to prevent document creation or opening in certain cases) rather than catching the situation after document initialization has already begun.

Implement awakeFromNib to initialize objects unarchived from the document’s window nib files (but not the document itself).

Moving Document Data to and from iCloud

The iCloud storage technology enables you to share documents and other app data among multiple computers that run your document-based app. If you have an iOS version of your document-based app that shares the same document data formats, documents can be shared among iOS devices as well, as shown in Figure 4-1. Changes made to the file or directory on one device are stored locally and then pushed to iCloud using a local daemon. The transfer of files to and from each device is transparent to your app.

Figure 4-1  Sharing document data via iCloud

Access to iCloud is controlled using entitlements, which your app configures through Xcode. If these entitlements are not present, your app is prevented from accessing files and other data in iCloud. In particular, the container identifiers for your app must be declared in the com.apple.developer.ubiquity-container-identifiers entitlement. For information about how to configure your app’s entitlements, see Developing for the App Store and Tools Workflow Guide for Mac.

All files and directories stored in iCloud must be managed by an object that adopts the NSFilePresenter protocol, and all changes you make to those files and directories must occur through an NSFileCoordinator object. The file presenter and file coordinator prevent external sources from modifying the file at the same time and deliver relevant notifications to other file presenters. NSDocument implements the methods of the NSFilePresenter protocol and handles all of the file-related management for you. All your app must do is read and write the document data when told to do so. Be sure you override autosavesInPlace to return YES to enable file coordination in your NSDocument object.

Determining Whether iCloud Is Enabled

Early in the execution of your app, before you try to use any other iCloud interfaces, you must call the NSFileManager method URLForUbiquityContainerIdentifier: to determine whether iCloud storage is enabled. This method returns a valid URL when iCloud is enabled (and the specified container directory is available) or nil when iCloud is disabled. URLForUbiquityContainerIdentifier: also returns nil if you specify a container ID that the app isn't allowed to access or that doesn't exist. In that case, the NSFileManager object logs a message to the console to help diagnose the error.

Listing 4-3 illustrates how to determine whether iCloud is enabled for the document’s file URL, presenting an error message to the user if not, and setting the value of the document’s destination URL to that of its iCloud container otherwise (in preparation for moving the document to iCloud using the setUbiquitous:itemAtURL:destinationURL:error: method).

Listing 4-3  Determining whether iCloud is enabled

NSURL *src = [self fileURL];
NSURL *dest = NULL;
NSURL *ubiquityContainerURL = [[[NSFileManager defaultManager]
                                 URLForUbiquityContainerIdentifier:nil]
                                 URLByAppendingPathComponent:@"Documents"];
    if (ubiquityContainerURL == nil) {
        NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
              NSLocalizedString(@"iCloud does not appear to be configured.", @""),
                              NSLocalizedFailureReasonErrorKey, nil];
        NSError *error = [NSError errorWithDomain:@"Application" code:404
                                         userInfo:dict];
        [self presentError:error modalForWindow:[self windowForSheet] delegate:nil
                             didPresentSelector:NULL contextInfo:NULL];
        return;
        }
        dest = [ubiquityContainerURL URLByAppendingPathComponent:
                                                          [src lastPathComponent]];

Because the message specifies nil for the container identifier parameter, URLForUbiquityContainerIdentifier: returns the first container listed in the com.apple.developer.ubiquity-container-identifiers entitlement and creates the corresponding directory if it does not yet exist. Alternatively, you could specify your app’s container identifier—a concatenation of team ID and app bundle ID, separated by a period for the app’s primary container identifier, or a different container directory. For example, you could declare a string constant for the container identifier, as in the following example, and pass the constant name with the message.

static NSString *UbiquityContainerIdentifier = @"A1B2C3D4E5.com.domainname.appname";

The method also appends the document’s filename to the destination URL.

Searching for Documents in iCloud

Apps should use NSMetadataQuery objects to search for items in iCloud container directories. Metadata queries return results only when iCloud storage is enabled and the corresponding container directories have been created. For information about how to create and configure metadata search queries, see File Metadata Search Programming Guide. For information about how to iterate directories using NSFileManager, see File System Programming Guide.

Moving a Document into iCloud Storage

To save a new document to the iCloud container directory, first save it locally and then call the NSFileManager method setUbiquitous:itemAtURL:destinationURL:error: to move the document file to iCloud.

Listing 4-4 shows an example implementation of a method that moves a file to iCloud storage. It assumes the source and destination URLs from Listing 4-3.

Listing 4-4  Moving a document to iCloud

dispatch_queue_t globalQueue =
        dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^(void) {
    NSFileManager *fileManager = [[NSFileManager alloc] init];
    NSError *error = nil;
    // Move the file.
    BOOL success = [fileManager setUbiquitous:YES itemAtURL:src
                               destinationURL:dest error:&error];
    dispatch_async(dispatch_get_main_queue(), ^(void) {
        if (! success) {
            [self presentError:error modalForWindow:[self windowForSheet]
                  delegate:nil didPresentSelector:NULL contextInfo:NULL];
        }
    });
});
[self setFileURL:dest];
[self setFileModificationDate:nil];

After a document file has been moved to iCloud, as shown in Listing 4-4, reading and writing are performed by the normal NSDocument mechanisms, which automatically manage the file access coordination required by iCloud.

Removing a Document from iCloud Storage

To move a document file from an iCloud container directory, follow the same procedure described in Moving a Document into iCloud Storage, except switch the source URL (now the document file in the iCloud container directory) and the destination URL (the location of the document file in the local file system). In addition, the first parameter of the setUbiquitous:itemAtURL:destinationURL:error: method should now be NO.

For clarity in this example, the URL of the file in iCloud storage is named cloudsrc and the local URL to which the file is moved is named localdest.

dispatch_queue_t globalQueue =
        dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^(void) {
    NSFileManager *fileManager = [[NSFileManager alloc] init];
    NSError *error = nil;
    // Move the file.
    BOOL success = [fileManager setUbiquitous:NO itemAtURL:cloudsrc
                               destinationURL:localdest error:&error];
 
    dispatch_async(dispatch_get_main_queue(), ^(void) {
        if (! success) {
            [self presentError:error modalForWindow:[self windowForSheet]
                  delegate:nil didPresentSelector:NULL contextInfo:NULL];
        }
    });
});

For more information about iCloud, see iCloud Design Guide.

NSDocument Handles Conflict Resolution Among Document Versions

NSDocument handles conflict resolution automatically, so you do not need to implement it yourself. If a conflict comes in while the document is open, NSDocument presents a sheet asking the user to resolve the conflict (or ignore, which marks it as resolved and accepts the automatic winner of the conflict, usually the one with the most recent modification date). Clicking Resolve invokes the Versions user interface (see Users Can Browse Document Versions) with only the conflicting versions visible. The user can choose a particular version and click Restore to make it the winner of the conflict, or just select Done to accept the automatic winner.

Even after the conflict is resolved, NSDocument always keeps the conflicting versions, and they can be accessed normally through Versions.

Optional Method Overrides

The areas described by items in the following sections require method overrides in some situations. And, of course, you must implement any methods that are special to your NSDocument subclass. More options for your NSDocument subclass are described in Alternative Design Considerations.

Window Controller Creation

NSDocument subclasses must create their window controllers. They can do this indirectly or directly. If a document has only one nib file with one window in it, the subclass can override windowNibName to return the name of the window nib file. As a consequence, the document architecture creates a default NSWindowController instance for the document, with the document as the nib file’s owner. If a document has multiple windows, or if an instance of a custom NSWindowController subclass is used, the NSDocument subclass must override makeWindowControllers to create these objects.

If your document has only one window, the project template provides a default implementation of the NSDocument method windowNibName:

- (NSString *)windowNibName {
    return @"MyDocument";
}

If your document has more than one window, or if you have a custom subclass of NSWindowController, override makeWindowControllers instead. Make sure you add each created window controller to the list of such objects managed by the document using addWindowController:.

Window Nib File Loading

You can implement windowControllerWillLoadNib: and windowControllerDidLoadNib: to perform any necessary tasks related to the window before and after it is loaded from the nib file. For example, you may need to perform setup operations on user interface objects, such as setting the content of a view, after the app’s model data has been loaded. In this case, you must remember that the NSDocument data-reading methods, such as readFromData:ofType:error:, are called before the document’s user interface objects contained in its nib file are loaded. Of course, you cannot send messages to user interface objects until after the nib file loads. So, you can do such operations in windowControllerDidLoadNib:.

Here is an example:

- (void)windowControllerDidLoadNib:(NSWindowController *)windowController {
    [super windowControllerDidLoadNib:windowController];
    [textView setAllowsUndo:YES];
    if (fileContents != nil) {
        [textView setString:fileContents];
        fileContents = nil;
    }
}

Printing and Page Layout

A document-based app can change the information it uses to define how document data is printed. This information is encapsulated in an NSPrintInfo object. If you want users to be able to print a document, you must override printOperationWithSettings:error:, possibly providing a modified NSPrintInfo object.

Modifying the Save Dialog Accessory View

By default, when NSDocument runs the Save dialog, and the document has multiple writable document types, it inserts an accessory view near the bottom of the dialog. This view contains a pop-up menu of the writable types. If you don’t want this pop-up menu, override shouldRunSavePanelWithAccessoryView to return NO. You can also override prepareSavePanel: to do any further customization of the Save dialog.

Validating Menu Items

NSDocument implements validateUserInterfaceItem: to manage the enabled state of the Revert Document and Save menu items. If you want to validate other menu items, you can override this method, but be sure to invoke the superclass implementation. For more information on menu item validation, see Application Menu and Pop-up List Programming Topics.