July 17th, 2018
Filed under: Cocoa, Mac Development | 5 comments
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.
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.
You can access your document type from Xcode’s project editor.
Go to the Document Types section and select the Bundle checkbox. Now your document will be saved as a 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:
Data
object.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"
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)
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)
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.
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
}
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: swift
Good article – it got right to the point with easy to understand code examples ?
Hey, this is a great help but I can’t work out how your examples would be for storing wrappers of pdf files. Any chance you would provide example ‘fileWrapper(ofType…’ and ‘read(from fileWrapper: FileWrapper, ofType…’ functions for pdf?
Francis,
I don’t have an example of using file wrappers with PDF files, but I can provide some help. To store a PDF file in a file wrapper, you must convert the contents of the file into a
Data
object. File wrappers work withData
objects. The Quartz framework has adataRepresentation
function that returns aData
object from a PDF document. You can use that function to write PDF files to a file wrapper. ThePDFDocument
class has an initializer that takes aData
object as an argument that you can use to read PDF files from a file wrapper. The PDFKIt and Quartz frameworks are the places to look for documentation on working with PDF files.Thanks for such a quick reply. I had managed to get what you told me in your reply but when I save a ‘Document’ it is only an empty folder.
I will keep trying but do let me know if anything springs to your mind.
Thanks again
Francis,
Make sure you select the Bundle checkbox in the Document Types section of the app target, as mentioned in the Making Your Document Type a File Wrapper section in the article. That will keep the file wrapper from appearing as a folder. I also recommend setting a breakpoint at the start of the function where you save the file wrapper and step through the code. Doing that should help you discover why the file wrapper is empty.