Using File Wrappers in a SwiftUI App

What Is a File Wrapper?

A file wrapper is a bundle, which is a collection of one or more directories (folders) and files that appears as a single file in the Finder (Mac) or Files app (iOS). Most Mac applications use bundles. If you want to see what a bundle looks like, select an application, right-click, and choose Show Package Contents.

When should you use a file wrapper? Use a file wrapper when you want to save your app’s data in multiple files. Suppose you’re developing a website building app. A website can have multiple pages, plus folders and files for images, videos, and custom CSS. By using a file wrapper you can save all these files and have it look like a single file to the person using the app. Most apps don’t need to use file wrappers

Types of File Wrappers

The FileWrapper class has the following file wrappers:

  • Directory
  • Regular file
  • Symbolic link

The symbolic link file wrapper points to a file. The most common case of using a symbolic link file wrapper is to point to a large file (image, audio, or video file) to keep the wrapper from getting too big. I’m going to focus on directory and regular file wrappers in this article.

Making Your File Wrapper Appear as a Single File

Document-based apps are more likely to use file wrappers than shoebox apps. If you forget to configure the document to be a file wrapper, the document will appear as a folder instead of a single file.

To configure the document to be a file wrapper, perform the following steps:

  1. Select the project from the project navigator to open the project editor.
  2. Select the app from the Targets list in the project editor.
  3. Click the Info button at the top of the project editor.
  4. Click the disclosure triangle next to the Exported Type Identifiers section in the project editor.
  5. Enter com.apple.package in the Conforms To text field.
  6. Click the disclosure triangle next to the Imported Type Identifiers section in the project editor.
  7. Enter com.apple.package in the Conforms To text field.

Creating a Document File Wrapper

You must create a file wrapper when saving the document. If you create a document-based SwiftUI app, you should see the following function in the document struct’s Swift file:

func fileWrapper(configuration: WriteConfiguration) 
  throws -> FileWrapper

This function is called when saving the document. Your code to create the file wrapper goes in this function.

To create a file wrapper you must perform the following tasks:

  • Create a directory file wrapper
  • Convert your app’s data to a Data object
  • Create a regular file wrapper
  • 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. You will return this wrapper in the call to fileWrapper. To create a directory file wrapper, call the FileWrapper method directoryWithFileWrappers and supply an empty Swift dictionary.

let mainDirectory = FileWrapper(directoryWithFileWrappers: [:])

Call directoryWithFileWrappers and supply an empty dictionary for any additional directories you want to create.

let pagesDirectory = FileWrapper(directoryWithFileWrappers: [:])

The root directory of a wrapper does not need a name because it does not appear in the Finder or Files app. But any other directories you create require a name. Set the preferredFilename property to name the directory.

pagesDirectory.preferredFilename = "Pages"

Call the addFileWrapper method to add the directory to the root directory.

mainDirectory.addFileWrapper(pagesDirectory)

Convert App Data to a Data Object

Files in file wrappers store their data in a Data object. You must convert your app’s data to Data to use file wrappers. Converting to Data depends on what you are storing, but the following code converts a string to Data:

let pageData = pageString.data(using: .utf8)

Creating a Regular 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: data)

Set the preferredFilename property to name the file.

wrapper.preferredFilename = "index.html"

In a real app you won’t be hardcoding filenames often. Suppose you have a document that has a list of pages. You would use the page’s title as the filename instead of giving the file a specific name. Remember that you use file wrappers to save multiple files. If you have 20 files to save, hardcoding the name of each file is going to be a pain. The following code demonstrates how to save a collection of text files:

// A page has a title and text
for page in pages {
  if let data = page.write() {
    let wrapper = FileWrapper(regularFileWithContents: data)
    wrapper.preferredFilename = page.title
    pagesDirectory.addFileWrapper(wrapper)
  }
}
    
func write() -> Data? {
  return text.data(using: .utf8)
}

Call addFileWrapper to add a file to a directory.

Reading from a File Wrapper

At this point you know how to create a file wrapper and save data to it. The next step is to read data from the file wrapper when opening a document.

The SwiftUI document structure provides the following initializer to read data from a file wrapper:

init(configuration: ReadConfiguration) throws

This initializer is where you add the code to read from the file wrapper. To read from a file wrapper, you must perform the following tasks:

  • Access the directory holding the files
  • Read the individual files
  • Load the data from the file

Access the Directory Holding the Files

If you store all the files inside the root directory, you won’t need to write any code to access the directory. But if you have files inside a subdirectory, you must call the fileWrappers method and supply the name of the subdirectory as the dictionary value.

if let wrappers = mainDirectory.fileWrappers {
  let pagesDirectory = wrappers["Pages"]
}

The SwiftUI initializer takes a read configuration as an argument. The ReadConfiguration struct has a file property that provides access to the root directory of the file wrapper.

init(configuration: ReadConfiguration) throws {
  guard let pagesDirectory = configuration.file.fileWrappers?["Pages"] else {
    throw CocoaError(.fileReadCorruptFile)
  }
}

Reading the Individual Files

Use the fileWrappers property to access the files in a directory file wrapper. Go through each file and load the data. Use the regularFileContents property to access the file contents.

if let pageFiles = pagesDirectory.fileWrappers {
  for pageFile in pageFiles {
    readPage(pageFile: pageFile.value)
  }
}
    
mutating func readPage(pageFile: FileWrapper) {
  if let filename = pageFile.filename,
    let fileData = pageFile.regularFileContents {
    
    // Create a page, load the data
    // from the file, and add it to an array.
    let page = Page()
    page.title = filename
    page.read(data: fileData)
    pages.append(page)
	}
}

Loading the Data

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. The following code loads a file’s contents into a string variable:

func read(data: Data) {
  if let fileContents = String(data: data, encoding: .utf8) {
    text = fileContents
  }
}

Sample Project

I have a sample project on GitHub that demonstrates saving data to a file wrapper. Look at the files Wiki.swift and Page.swift.

Get the Swift Dev Journal Newsletter

Subscribe and get exclusive articles, a free guide on moving from tutorials to making your first app, notices of sales on books, and anything I decide to add in the future.

    We won't send you spam. Unsubscribe at any time.