13
May
2008
 

From Hacker to microISV: Custom File Formats

by Matt Long

In this continuing series on my own transition from a Mac application hacker to microISV (Independent Software Vendor), I am going to demonstrate how to create your own file format for your application. You’ve probably seen these types of files in popular applications such as iMovie HD (06′) or GarageBand or even xcode in which the actual files used are, behind the scenes, folders that the operating system treats as regular files. These folders/files have a special bit set on them that tell OS X how to deal with them. The goal of this post is to demonstrate how you can do the following:

  • Create your own file format with your own file extension
  • Register your file format with the operating system
  • Provide application loading of your file that’s been double clicked in the Finder
  • Write data and preferences back out to your application file
  • Add resources such as media files to your application file

You can download the demo application here: PDF Keeper Demo Application

A Brief Discussion On Files and Folders

If you’ve ever come across a file in your file system that when control-clicked gives you an option in the pop-up menu to Show Package Contents then you are familiar with the type of file we are looking to create. What you may not know, however, is that this file is actually a directory. The choice to show the package contents is the OS allowing you to treat the file as a folder, which it actually is behind the scenes. When you create a custom file format in this manner, you are simply storing your project data and files under a sub directory.

You can verify that a special bit is set on these types of files by opening Terminal.app, navigating to the directory where such a file resides and running ls -al to get a detailed file list. When I do that on my directory where some of my custom files types are stored, I get a listing that looks like this.

PDF Keeper Project Files in Terminal

Notice the at symbol (@) in the file listing. This is the visual indicator that this directory has extended information–which in our case means that the directory is being treated as a package. If you want to set this bit through the command line, just run the following command:

SetFile -a B <directory_name>

Where <directory_name> is the name of the directory on which you would like to set this bit.

PDF Keeper Demo Application

For this post, I’ve included a demo application I’ve named PDF Keeper. PDF Keeper isn’t terribly practical as an application, but it effectively demonstrates the principles I’m wanting to convey. It simply opens our custom file type and loads in properties and data specified in the Info.plist file that each custom file contains. When you first run the application, you will be prompted to create a new PDF Keeper file. You will then see a list view that displays a list of PDF files stored within our custom file type. When you first create the file, the list veiw will be blank, so you will need to add some PDF files by selecting File | Add PDF File…. It then allows you to double-click any of the files in the list to view them in a PDFView.

If you were to look at one of the PDF Keeper file contents by selecting Show Package Contents from the context menu in the finder, you would see file hierarchy similar to the one below.

PDF Keeper Hierarchy

Note: Keep in mind that you will have different PDF files in the Resources directory than I have.

The Info.plist file inside the Contents directory is a property list file in XML format that keeps track of the PDF files we are storing. The Resources directory is where all of the PDF files are actually stored when we add one in PDF Keeper. Download, build and run the demo application to see it in action.

The path to your Info.plist file is important. It should be located inside of the Contents directory and be named Info.plist. That way, when we call, -[NSBundle bundleWithPath:], we will receive back an NSBundle whose infoDictionary member variable is an NSDictionary populated with the contents of the Info.plist file.

You can see how we do this in the call to -setupProject.

- (void)setupProject:(NSString*)projectPath;
{
  currentProjectFilepath = projectPath;

  // Load the file bundle to obtain the bundle dictionary
  NSBundle *contextBundle = [NSBundle bundleWithPath:projectPath];
  if (![contextBundle infoDictionary]) return;
    
  // The infoDictionary represents the Info.plist file. We load it
  // and grab the array of files using the key "Files"
  NSArray *files = [[contextBundle infoDictionary] objectForKey:@"Files"];
    
  // If we have a list of files, we initialize our array model with them.
  // Otherwise we initialize the array with a capacity of 10
  if( files != nil )
  {
      pdfFiles = [[NSMutableArray alloc] initWithArray:files];
  }
  else
  {
      pdfFiles = [[NSMutableArray alloc] initWithCapacity:10];
  }
    
  // Reload the table view
  [tableView reloadData];
}

Create Your Own File Format

This is probably the most tedious aspect of the custom file format task as you have to decide what data you want your file to contain. I suggest that you code up a sample property list file in xcode and manually edit it to represent what you want. Then you can tweak it as you go. You won’t think of everything you want to save at first, so you do this loosely, however, you do need to start somewhere. This property list file is what will be stored inside the Contents folder of your custom file. Here is what a Info.plist file looks like in a PDF Keeper custom file.





  Files
  
    About Stacks.pdf
    8051 Microcontroller.pdf
  


Register Your Custom File Format

This part is actually quite simple. You will, however, want to have your own custom icon ready to use for your custom format file. To register your file format, which is what enables you to double click your file in the Finder and have it properly load your application, with your project loaded in xcode, select Project | Edit Active Target “<Project Name>” where <Project Name> is the name of your project. In the ensuing window, select the Properties tab. You will see at the bottom a list view labeled Document Types:. This is where you will specify your document name, extension, and icon file. Below is a screenshot of the Document Type specified in PDF Keeper.

PDF Keeper Doc Types

Notice that the Package checkbox is checked. This is what tells Finder that your file, though a directory, should look like a single file to the end user. You should also make note that the file extension field does not have a dot (.) in it. I learned that one the hard way.

Hiding/Showing File Extensions

Some applications that use this technique for custom file formats hide the file extension while others show it. Garageband, the entry level music recording application from Apple, for example, hides its file extension which is .band while our beloved xcode shows its file extension which is .xcodeproj.

Hiding and showing file extensions is handled in the Finder through the Get Info… window. If you select a Garageband file in the finder and press Command-I you will notice under the Name & Extension section the Hide extension checkbox is checked.

Here is a screenshot of a Garageband file’s Get Info… information.

Garageband File Info

Note: If you don’t have any Garageband files to look at, just run the demo app, PDF Keeper and create a new file. You can then Get Info… by pressing Command-I with that file selected in the finder to see the same thing.

You can do the same with an xcode project and you will notice that its Hide extension checkbox is not checked.

So the question becomes, how do we handle this in the code. When you create a new file of your custom format, you’ll want to control this, so how do we do so? It is a simple attribute you specify in an NSDictionary before calling NSFileManager’s createDirectoryAtPath called NSFileExtensionHidden. The code looks like this in PDF Keeper

NSNumber *num = [NSNumber numberWithBool:YES];
NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:num, NSFileExtensionHidden, nil];
if( ![[NSFileManager defaultManager] fileExistsAtPath:filepath] )
{
  [[NSFileManager defaultManager] createDirectoryAtPath:filepath attributes:attribs];
}

Keep in mind that you will also want to control whether or not file extensions can be seen in your NSSavePanel and whether or not the OS should treat file packages as directories. The following code is how we do this in PDF Keeper

[savePanel setCanSelectHiddenExtension:NO];
[savePanel setTreatsFilePackagesAsDirectories:NO];

If canSelectHiddenExtension is set to YES, the user would have the option to select/deselect a checkbox in the save file panel to show or hide the extension. We simply hide it for PDF Keeper as it reduces confusion and makes it easier to manage everything behind the scenes.

If treatsFilePackagesAsDirectories is set to YES, the file system would allow the user to drill down into the custom file as if it were a regular directory. We don’t want the user to be able to do that as we are treating the custom file as a normal file rather than a directory.

Loading Your Custom File From the Finder

You will likely want to enable your users to double click a file in the Finder that will then automatically load into your application. If you added a document type to your project as specified in the Register Your Custom File Format section above, then you are already part of the way there as on the first run of your application, the file/document type is automatically registered with the OS so that double clicks of your file type will be handed to your application without any further effort on your part.

Now you just need your application to be able to respond when it gets notified that the Finder wants you to load your files type. Fortunately, there is a very simple way to handle this scenario. All you need to do is implement the delegate

- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename

in your app delegate. The filename parameter contains the path to the file that was double-clicked in the Finder. You simply do all of your file loading/initialization when this method gets called. You may also implement

- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames

if you want to handle multiple files being passed to your application. The following code snippet shows how these two delegate methods are implemented in PDF Keeper

- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
{
  // This gets called when the user double-clicks a project file in the Finder.
  [self openProject:filename];
  [self setupProject:filename];
  return YES;
}

- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
{
  // This gets called when the user opens multiple files from the Finder.
    
  // We only take the first file.
  if( [filenames count] > 0 )
  {
      [self application:sender openFile:[filenames objectAtIndex:0]];
  }
}

Importing Resource Files

In the PDF Keeper demo application, we are only concerned with PDF files. When users select File | Add PDF File… they are prompted to select a PDF file that is then imported into the custom file type. This importing procedure is really just a simple file copy. Remember that our custom file is really just a directory that is being treated as a regular file by the OS. The file import just copies the file from the place where it was selected by the user into the Resources directory of our custom file. Here is the code to add a PDF file to our project file.

- (IBAction)addPDFFile:(id)sender;
{
  NSOpenPanel *openPanel;
	
  openPanel = [NSOpenPanel openPanel];
  [openPanel setCanChooseDirectories:NO];
  [openPanel setAllowsMultipleSelection:NO];
  [openPanel setResolvesAliases:YES];
  [openPanel setCanChooseFiles:YES];
	
  NSArray *fileTypes = [NSArray arrayWithObjects: @"pdf", nil];
    
  if ([openPanel runModalForTypes:fileTypes] == NSOKButton)
  {
      // Build our destination filename based upon the current project
      // file path and the Contents/Resources path below it.
      NSString *filename = [openPanel filename];
      NSString *displayName = [[NSFileManager defaultManager] displayNameAtPath:filename];
      NSString *destFile = [currentProjectFilepath stringByAppendingPathComponent:@"Contents"];
      destFile = [destFile stringByAppendingPathComponent:@"Resources"];
      destFile = [destFile stringByAppendingPathComponent:displayName];
        
      // Use the NSFileManager to copy the file
      [[NSFileManager defaultManager] copyPath:filename toPath:destFile handler:nil];

      if( pdfFiles != nil )
      {
          // Add the filename to our list of files.
          [pdfFiles addObject:displayName];
          // Save the project.
          [self saveProject:currentProjectFilepath];
          // Reload the table view
          [tableView reloadData];
      }
  }
}

Note: When you import the file you are simply copying it. If the file is large, though, you will want to provide a progress indicator so the user isn’t wondering if the app is hung or has crashed. This is non-trivial and will require you to use Carbon APIs to accomplish. In our demo app, I am simply calling NSFileManager’s copyTo: method which will copy the file, but will block until the copy is completed. If you want to implement a file copy using a progress indicator, see my previous post called Cocoa Tutorial: File Copy With Progress Indicator

Updating Info.plist Property List File

Whenever your user selects File | Save you want to update your Info.plist file with the contents of the pdfFiles NSDictionary that is used as the model for the table view control. To do so, we simply get the full path to the Info.plist file, delete the current file, and then save a new file to the same path. Here is the code we use to do so in PDF Keeper

// Set the path for our property list file.
NSString *plistFile = [contentsPath stringByAppendingPathComponent:@"Info.plist"];
    
// If it already exists in the project, delete it.
if( [[NSFileManager defaultManager] fileExistsAtPath:plistFile] )
  [[NSFileManager defaultManager] removeFileAtPath:plistFile handler:nil];
    
// Create a dictionary to use to write out our property list file.
NSDictionary *output;
output = [NSDictionary dictionaryWithObjectsAndKeys:pdfFiles, @"Files", nil];

// Write the file.
[output writeToFile:plistFile atomically:YES];

Conclusion

Your custom file format can be as simple or complex as you would like to make it. You simply decide on all of the information you would like to store in your Info.plist file and then create sub-directories under your Contents directory where you will store your data. The concepts are simple. You are storing data and information about the data inside of a directory that the filesystem treats as a single file. It’s a very elegant way to create and manage your own custom file formats. Until next time.

Comments

[…] Custom File Formats in Cocoa For the nth time Matt Long posts a spectacular, time-saving, hand-holding, super-insightful Cocoa tutorial about a month after I figured it out myself, the hard way. […]

[…] public links >> fileformats From Hacker to microISV: Custom File Formats Saved by learnfrommylife on Fri 31-10-2008 Different Digital File Formats used in Photoshop Saved […]

sfsam says:

Thanks for this.

I changed the package icon in my icon.icns, but I still see the old icon in the Finder on both packagest that were there before the change and new ones made after.

How do I force the Finder to use a new package icon if/when I change it?

Matt Long says:

@sfsam

Have you tried re-starting the Finder? That should do it.

-Matt

p.s. I asked Marcus. This was his suggestion.

sfsam says:

@Matt

I don’t know how to restart the Finder :o so I did the next best thing and restarted the Mac. That seems to have worked. Thank you.

I’m surprised this is the way to get updates to package icons to show up. If I change the application’s icon, I will see the new icon when I build and run the app. If I send an update to users (say via Sparkle), they’ll see the new app icon. I’m surprised that the only way they’d see an updated package icon is if they restarted the Finder. I don’t plan on changing icons a lot so it’s not a big issue, but sometimes for a huge update (say a major version), you might do this.

By the way, great blog. Lots of great info.

mubashir441 says:

How i can update a plist file, for example add or delete an item from it and save it again.

Matt Long says:

@mubashir441

Load it into a dictionary. Add and delete the items from the dictionary and then serialize the dictionary to disk again.

If that’s not clear, ask your question on Stack Overflow.

Best Regards.