SwiftUI Importing And Exporting Files

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.

  1. 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
    
  2. 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")
          }
        }
      }            
    }
    
  3. 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:

File picker with two documents

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:

  1. Add a property for the presentation state of the file picker:

    @State private var isExporting = false
    
  2. Add a toolbar button to trigger the presentation:

    .toolbar {
      ToolbarItem() {
        Button {
          isExporting = true
        } label: {
          Label("Export file", systemImage: "square.and.arrow.up")
        }
      }
    }
    
  3. 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> 
}