How do you import and export files with SwiftUI?
Importing a File
I showed an example of importing a file when discussing accessing security scoped files. Here’s a recap of the key steps.
-
Add a property somewhere in the view hierarchy to store the presentation state of the file picker:
struct ContentView: View { @State private var text = "" @State private var error: Error? @State private var isImporting = false
-
Add a button to trigger the presentation of the file picker. For example, I’m using a toolbar button:
var body: some View { VStack { TextEditor(text: $text) .padding() if let error { ErrorView(error: error) } } .toolbar { ToolbarItem() { Button { isImporting = true } label: { Label("Import file", systemImage: "square.and.arrow.down") } } } }
-
Add the
fileImporter
view modifier to the view:
VStack { ...
}
.fileImporter(isPresented: $isImporting,
allowedContentTypes: [.text]) {
let result = $0.flatMap { url in
read(from: url)
}
switch result {
case .success(let text):
self.text += text
case .failure(let error):
self.error = error
}
}
The fileImporter
takes a binding to the presentation state property and a list of supported content types. When we set isPresented
to true in the button action SwiftUI presents the file picker:
The allowedContentTypes
is an array of UTType
identifiers for the file types that you want to import. Common types include .text
, .data
, .html
, .jpeg
, .json
, .xml
, etc. For the full list see the UTType documentation. If you need to define your own custom type see the WWDC18 session Managing Documents In Your iOS Apps.
SwiftUI calls the completion handler when the operation completes with a single Result
type containing either the URL
of the selected file or an Error
. The completion handler is not called if the user cancels the operation.
If the user chooses a file outside of the app’s container you’ll get an access error when reading the file unless you use the security scope associated with the url. See accessing security scoped files for details:
private func read(from url: URL) -> Result<String,Error> {
let accessing = url.startAccessingSecurityScopedResource()
defer {
if accessing {
url.stopAccessingSecurityScopedResource()
}
}
return Result { try String(contentsOf: url) }
}
There’s a second version of the fileImporter that allows multiple file selection. The completion handler then gets a Result
type that contains a collection of URL’s:
.fileImporter(isPresented: $isImporting,
allowedContentTypes: [.text],
allowsMultipleSelection: true) { result in
// Result<[URL]>, Error>
}
Exporting a File
When exporting a file you need some extra boilerplate code to define a file document. I’m working with text so my document is a wrapper around a String
:
struct TextDocument: FileDocument {
var text: String = ""
init(_ text: String = "") {
self.text = text
}
}
There are three requirements for a FileDocument
. The first is to list the content types the document supports:
import UniformTypeIdentifiers
struct TextDocument: FileDocument {
static public var readableContentTypes: [UTType] =
[.text, .json, .xml]
The second requirement is the initializer that loads documents from a file. The configuration parameter gives us access to the file wrapper whose contents we convert to text:
init(configuration: ReadConfiguration) throws {
if let data = configuration.file.regularFileContents {
text = String(decoding: data, as: UTF8.self)
}
}
The third requirement is the method that goes the other way creating a FileWrapper
representing the file on disk:
func fileWrapper(configuration: WriteConfiguration)
throws -> FileWrapper {
let data = Data(text.utf8)
return FileWrapper(regularFileWithContents: data)
}
Back in our SwiftUI view we follow a similar pattern to importing a file:
-
Add a property for the presentation state of the file picker:
@State private var isExporting = false
-
Add a toolbar button to trigger the presentation:
.toolbar { ToolbarItem() { Button { isExporting = true } label: { Label("Export file", systemImage: "square.and.arrow.up") } } }
-
Add the
fileExporter
view modifier to the view:.fileExporter(isPresented: $isExporting, document: TextDocument(text), contentType: .text, defaultFilename: "document.txt") { result in if case .failure(let error) = result { self.error = error } }
The fileExporter takes a binding to the presentation state and an optional document. Both must be true before SwiftUI shows the file picker.
The contentType
parameter must match one of the content types we listed in the document. I’ve also included a default filename for the exported file, the user can change this in the file picker.
Unless the user cancels the export, SwiftUI calls the completion handler with a Result
containing the URL
of the exported file or an Error
.
Like the importer, there’s also a version of the fileExporter that supports exporting multiple documents:
.fileExporter(isPresented: $isExporting,
documents: exportDocuments(),
contentType: .text) { result in
// Result<[URL], Error>
}