Working with Cocoa File Packages

October 27th, 2010

Filed under: Cocoa, Mac Development | 3 comments

Saving a Cocoa application’s data in a file package is not much more difficult than saving the data in a single file. This post shows you how to save your document’s data in a file package and read the data from the package.

Introduction to File Packages

A file package is a bundle, which is a collection of one or more directories that appears as a single file to the user. Xcode projects use file packages. Select an Xcode project in the Finder, right-click, and choose Show Package Contents to examine the file package.

When should you use a file package? Use a file package when you want to save your application’s document data in multiple files. A game level editor may want to save the level layout, the level’s enemy list, and the level’s treasure list in separate files. Screenwriting software may want to save each scene in its own text file.

The simplest file package is a single directory that contains all the files. But you can have multiple directories to group files if you’re going to store lots of files in your file package.

There are two tasks you must perform to add file package support to your application. First, you must add a document type for the package to your Xcode project’s target. Second, you must implement the methods fileWrapperOfType: and readFromFileWrapper: in your NSDocument subclass.

Adding a Document Type to Your Project

By adding a document type to your project’s target and setting the document type as a package, your document’s data will be saved as a file package. If you don’t add the document type, your data will appear as a folder in the Finder instead of as a single file, which makes it easier for someone to accidentally delete or move a file.

To access the inspector for your target, perform the following steps in Xcode:

  1. Select your target from the Groups and Files list.
  2. Click the Info button on the project window toolbar to open the target’s inspector.
  3. Click the Properties tab in the inspector.

At the bottom of the target inspector is a list of document types. You should see the following columns of information:

  • Name
  • UTI
  • Extensions
  • MIME Types
  • OS Types
  • Class
  • Icon File
  • Store Type
  • Role
  • Package

If you have a document-based Cocoa application project, there should be one document type in the list. Click the + button in the lower left corner of the inspector to add a document type if you need another one.

Mandatory Document Type Fields for File Packages

At a minimum, you must deal with the Name, Extensions, Role, and Package fields for your file package’s document type. For the Name field, give a description for your file type. For the Extensions field, list the file extension you want for your file type, skipping the period. If you wanted your file to have the extension .xyz, you would enter the following for the Extensions field:

xyz

Make sure the Role is Editor. An editor can read and write files. Select the Package checkbox to make your document type a file package.

The Other Document Type Fields

UTI stands for uniform type identifier, which identifies an abstract type, such as a file format. A UTI takes the following form:

com.CompanyName.DocumentType

MIME (Multimedia Email Extensions) types are file types a web browser uses. Web browsers don’t open file packages so you shouldn’t have to worry about setting MIME types.

OS Types are four-character codes for the document’s file type. Before Mac OS X, Mac applications used OS Types instead of file extensions to identify themselves as the creator of a particular file. Today, you should use file extensions and UTIs to identify a document type.

The Class field contains the name of your NSDocument subclass. The Icon File field contains the file name for the document icon. If you use a custom icon for your file package, enter the file name in the Icon File field. You should also add the icon file to your project.

The Store Type field is used by Core Data applications. If you’re not using Core Data, you can ignore this field.

Writing to a File Package

To write your document’s data to a file package, your NSDocument subclass must implement the method fileWrapperOfType:. This method takes the following form:

- (NSFileWrapper )fileWrapperOfType:(NSString )typeName error:(NSError **)outError

The file wrapper that fileWrapperOfType: returns is the main directory for the file package. The typeName argument identifies the type of document. You shouldn’t have to worry about the typeName argument. If there is a problem during the write, the outError argument will contain the reason for the problem.

In your implementation of fileWrapperOfType:, you must perform the following tasks:

  • Create a directory file wrapper.
  • Get your data into a NSData object.
  • Create a file wrapper and add the file to a directory file wrapper.

Creating a Directory File Wrapper

Call NSFileWrapper’s initDirectoryWithFileWrappers: method to create a directory file wrapper. The following code creates an empty directory:

NSFileWrapper* mainDirectory;
[mainDirectory initDirectoryWithFileWrappers:nil];

If you’re going to store all your files in one directory, you’re done with the directory file wrapper for now. But if you’re going to store your files in folders inside the file package, you must create a directory file wrapper for each folder and give that folder a preferred filename. Call NSFileWrapper’s setPreferredFilename: method to give the folder a preferred filename. The following code creates an empty directory named HTML inside the file package:

NSFileWrapper* htmlDirectory;
[htmlDirectory initDirectoryWithFileWrappers:nil];
[htmlDirectory setPreferredFilename:@“HTML”];

Getting Your Data into a NSData Object

NSFileWrapper’s methods use NSData objects to write data to files and read data from files. How you get your data into a NSData object depends on your application, but the following code shows how to convert the RTF data of a text view to NSData:

NSTextView* textView;
NSData* myData;
NSRange range = NSMakeRange(0, [[textView textStorage] length]);
myData = [[textView textStorage]RTFFromRange:range documentAttributes:nil];

The following code demonstrates how to convert an XML document to NSData:

NSXMLDocument* xmlDoc;
NSData* myData = [xmlDoc XMLData];

Creating a File Wrapper

You can create a file wrapper and add it to a directory file wrapper with one method call. Call NSFileWrapper’s addRegularFileWithContents: method. This method takes two arguments. The first argument is the NSData object you created, and the second argument is the filename you want for the file. The following code adds a file named “index” to the HTML directory:

NSFileWrapper* htmlDirectory;
NSData* myData;
[htmlDirectory addRegularFileWithContents:myData preferredFilename:@”index”];

Call addRegularFileWithContents: for each file you’re going to save in the file package.

To add a subdirectory to the directory, call NSFileWrapper’s addFileWrapper: method. The following code adds the HTML directory to the main directory:

NSFileWrapper* htmlDirectory;
NSFileWrapper* mainDirectory;
[mainDirectory addFileWrapper:htmlDirectory];

Reading from a File Package

To read from a file package, your NSDocument subclass must implement the method readFromFileWrapper:. This method takes the following form:

- (BOOL)readFromFileWrapper:(NSFileWrapper )fileWrapper ofType:(NSString )typeName error:(NSError **)outError

The fileWrapper argument is the main directory where all the files in the wrapper are stored. You can give it a more descriptive name than fileWrapper if you want. The typeName argument identifies the type of document. You shouldn’t have to worry about the typeName argument. If there is a problem during the read, the outError argument will contain the reason for the problem.

If you have all your files inside the main wrapper, call NSFileWrapper’s fileWrappers: method, which contains all the file wrappers inside a directory.

NSFileWrapper* mainDirectory;
NSDictionary* files = [mainDirectory fileWrappers];

If you need to find a particular file or directory, call NSFileWrapper’s objectForKey: method. The following code locates the HTML folder in the file wrapper:

NSFileWrapper* mainDirectory;
NSFileWrapper* htmlDirectory = [[mainDirectory fileWrappers] objectForKey:@”HTML"];

Once you get a directory, call fileWrappers: to get the list of files in that directory.

NSFileWrapper* htmlFiles = [htmlDirectory fileWrappers];

Reading the Individual Files

When you call NSFileWrapper’s fileWrappers: method, it returns a NSDictionary object containing all the files in the directory. Assuming you want to read all the files, the easiest thing to do is create an NSEnumerator object and use that object to go through the files.

A dictionary contains a list of key-value pairs. The enumerator for a dictionary can either be a key enumerator or an object enumerator. For a file wrapper you should use a key enumerator that contains all of the filenames in the dictionary.

NSDictionary* files;
NSEnumerator* fileEnumerator = [files keyEnumerator];

Use NSEnumerator’s nextObject: method to go through the files in the dictionary. Call NSEnumerator’s objectForKey: method to get the file wrapper. Call NSFileWrapper’s regularFileContents: method to get the contents of the file in a NSData object.

id currentFilename;
NSFileWrapper* currentFile;
NSData* myData;

while (currentFilename = [fileEnumerator nextObject]) {
    currentFile = [files objectForKey:currentFilename];
    myData = [currentFile regularFileContents];
    // Load myData.
}

Loading the Data

How you load the data depends on the data you’re storing. If you store RTF data, you can use NSTextStorage’s initWithRTF: method to load a text view with the contents of a text file.

NSTextView* textView;
NSData* myData;
[[textView textStorage] initWithRTF:myData documentAttributes:nil];

If you’re loading an XML document, call NSXMLDocument’s initWithData: method to load the document from disk to the NSXMLDocument.

NSError* error;
NSXMLDocument* xmlDoc;
[xmlDoc initWithData:myData options:NSXMLDocumentValidate error:&error];

Additional Information

Read Apple’s documentation for the NSFileWrapper, NSData, and NSDocument classes for more information on working with file packages.


3 thoughts on “Working with Cocoa File Packages

  1. Kaz says:

    Thank you, this is great articles. Many of articles don’t talk about the purpose of this architecture and background, and explain only about API like the way, “read file wrappers and process what ever you need to.” or something similar… Excuse me… I had some idea about NSFileWrapper but there were too many missing pieces, and I feel like I finally found those.

    I have a question, if you need to supply data for addRegularFileWithContents: to creating a packaged document, it would be pretty memory intensive if you trying work on like site management type of app that requires to store thousands of file. I thought packaging is designed to avoid reading nor writing whole data structure. Or, do I still have some missing pieces?

  2. Mark Szymczyk says:

    Kaz,

    I have not tried saving a file package with thousands of files, but I think the size of the files being saved would have the greatest impact on memory use. Saving lots of large files would be more memory intensive than saving lots of small files.

    The main purpose of packaging is to make a folder of files look like a single file to the user. Let’s take your site management app example. If you did not use a file package, you would end up saving a folder with thousands of files inside it. The user could delete or rename some of the files, which could cause problems if the user tries to run the app and load the file. Using a file package makes it more difficult for users to delete or rename files the app needs.

  3. berfis says:

    A good thing with file wrappers is to organize information much better as writing a huge .plist file.
    A second point is that file wrappers are, for occasional users, much easier to handle, rename, move, join to an e-mail and so on.
    A third advantage is that the file wrapper is (relatively) opaque for smarter users. I’m writing a game, and this opacity is useful to hide some critical features.

    Note: I’m using the Applescript-Cocoa bridge named “ApplescriptObjC”, not well documented but easy to learn for an AS scripter. So I have to “translate” a bit Obj-C code examples like yours, but it’s not so difficult. Your article about file wrappers is welcome! Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *