Working with File Wrappers in Swift

July 17th, 2018

Filed under: Cocoa, Mac Development | Be the first to comment!

Back in 2010 I wrote an article on working with file wrappers. Recently I needed to work with file wrappers, and I noticed the file wrapper code is much different in Swift so I figured working with file wrappers in Swift would be a good topic for an article.

This article assumes you’re creating a document-based application. I have only used file wrappers in document-based applications.

What Is a File Wrapper?

A file wrapper is a bundle, which is a collection of one or more directories that looks like a single file in the Finder. Most Mac applications are bundles. You can examine their contents by selecting an application, right-clicking, and choosing Show Package Contents.

When should you use a file wrapper? Use a file wrapper when you want to save your application’s document data in multiple files. A level editor for a game may want to save the level layout, the level’s enemy list, and the level’s treasure list in separate files.

The simplest file wrapper is a single directory that stores all the files. But you can create multiple directories to store your files to keep them better organized.

There are three tasks to perform to add file package support to a document-based application. First, you must tell Xcode that your document type is a file wrapper. Second, you must implement the fileWrapperOfType method in your NSDocument subclass to save to a file wrapper. Third, you must implement the readFromFileWrapper method in your NSDocument subclass to open a file wrapper and read its contents.

Making Your Document Type a File Wrapper

You can access your document type from Xcode’s project editor.

  1. Select your project from the project navigator to open the project editor.
  2. Select the application target from the target list on the left side of the project editor.
  3. Click the Info button at the top of the project editor.

DocumentTypeAsBundleHighlighted

Go to the Document Types section and select the Bundle checkbox. Now your document will be saved as a file wrapper.

Writing a Document File Wrapper

To write a document file wrapper, you must override and implement the fileWrapperOfType method. Here is the Swift signature for fileWrapperOfType.

override func fileWrapper(ofType typeName: String) throws -> FileWrapper

The file wrapper fileWrapperOfType returns is the root directory for the file wrapper.

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

  • Create a directory file wrapper.
  • Get your app’s data into a Data object.
  • Create a file wrapper and add the file to a directory file wrapper.

Creating a Directory File Wrapper

At a minimum you must create a root directory for the file wrapper. To create a directory in a file wrapper, call the FileWrapper method directoryWithFileWrappers and supply an empty Swift dictionary.

let rootDirectory = FileWrapper(directoryWithFileWrappers: [:])

If you want to add any other directories to your file wrapper, call directoryWithFileWrappers again and supply an empty Swift dictionary.

let htmlDirectory = FileWrapper(directoryWithFileWrappers: [:])

The root directory in a file wrapper does not need a name, but any other directories you create need a name. Set the filename or preferredFilename property for the directory file wrapper to name the directory. The following code gives the HTML directory the name HTML:

htmlDirectory.filename = "HTML"

Getting Your Data into a Data Object

Files in file wrappers store their data in a Data (formerly NSData) object. Converting to Data depends on what you are storing, but the following code converts a string to Data:

let htmlData = htmlString.data(using: .utf8)

Creating a File Wrapper

To create a file wrapper for a regular file, call the FileWrapper method regularFileWithContents and supply the Data object that contains the file’s data.

let wrapper = FileWrapper(regularFileWithContents: htmlData)

Set the filename or preferredFilename property to name the file.

wrapper.filename = "index.html"

In a real app you won’t be hardcoding filenames much. Suppose you have a document that has a list of articles. You would use the title of the article as the filename instead of giving the file a specific name. Remember that you use file wrappers to store multiple files. If you have 20 files to save, hardcoding the name of each file is going to be a pain.

After creating the file wrapper, call the directory’s addFileWrapper method to add it to the directory.

htmlDirectory.addFileWrapper(wrapper)

Call addFileWrapper to add a subdirectory to a file wrapper. The following code adds the HTML directory to the root directory:

rootDirectory.addFileWrapper(htmlDirectory)

Reading from a File Wrapper

At this point you know how to write to a file wrapper. The next step is to read the file wrapper. To read the file wrapper, you must override the readFromFileWrapper method. Here is the signature for readFromFileWrapper.

override func read(from fileWrapper: FileWrapper, ofType typeName: String) throws

The fileWrapper argument is the root directory of the file wrapper.

The fileWrappers property contains the contents of a file wrapper. The contents appear in a Swift dictionary, where the key is the name of a file or directory and the value is a file wrapper, which can be either a file or a directory.

To access a directory or file from the dictionary, supply the key as the dictionary subscript. The following code shows how to access a HTML directory inside the root file wrapper:

if let wrappers = fileWrapper.fileWrappers {
    let htmlDirectory = wrappers["HTML"]
}

Make sure the subscript you supply matches the filename or preferred filename you gave to the file or directory. If you make a typing mistake, you won’t be able to read from the file wrapper.

Reading the Individual Files

Use the fileWrappers property to access the individual files in a file wrapper directory. The following code provides a list of files inside a HTML directory:

htmlFiles = htmlDirectory.fileWrappers

The contents appear in a Swift dictionary, where the key contains the name of the file and the value contains a file wrapper.

Use the regularFileContents property to access the contents of a file. The regularFileContents property has type Data?, which means it’s either a Data object or nil. By using regularFileContents, you can get the Data object.

if let homepage = htmlFiles["index.html"] {
    let homepageData = homepage.regularFileContents
}

Loading the Data

Accessing the file wrapper’s regularFileContents property gives you a Data object to load the file’s contents in your app. Loading the file’s contents depends on the type of data being stored, but the following code loads a file’s contents into a string variable:

let htmlString = String(data: homepageData, encoding: .utf8)

Tags:


Leave a Reply

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