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:
- Select the project from the project navigator to open the project editor.
- Select the app from the Targets list in the project editor.
- Click the Info button at the top of the project editor.
- Click the disclosure triangle next to the Exported Type Identifiers section in the project editor.
- Enter
com.apple.package
in the Conforms To text field. - Click the disclosure triangle next to the Imported Type Identifiers section in the project editor.
- 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
.