The File Importer in SwiftUI

September 29th, 2025

⏱ Reading Time: 4 mins

It might not be an often case, but apps sometimes need to access and import files out of their sandbox, such as the device they run on or the iCloud Drive. To manage that, there is a system-provided picker that does all the heavy job, and SwiftUI makes it easy to integrate it to any app with the fileImporter view modifier. Let’s get to know it, and how to properly read files once presented.


Presenting the file importer

To meet and demonstrate how everything works properly, we’ll work on a really simple app that lets us present the built-in file picker, select one or more text files, process them, and show their content in a SwiftUI list.

The file picker is presented using the fileImporter view modifier, and similarly to sheets and alerts, we need a state property to control whether it’s presented or not:

A button triggers its appearance as shown next:

The fileImporter modifier usually accepts four arguments:

  1. The binding value of the presented state property ($showFileImporter).
  2. A collection of the desired content types (file types) to view and pick.
  3. A boolean value indicating whether multiple file selection is allowed or not.
  4. A completion handler to handle the selected file or files.

In this example we’ll set the .text content type as we’ll open text files, and allow multiple file selection:

Note that, in order to make the available content types visible in the source code, it’s necessary to import the UniformTypeIdentifiers framework:

The result parameter in the completion handler is a Result<[URL], any Error> value. It contains:

  • Either a collection of URL objects,
  • or an error if importing fails for some reason.

Having seen how to prepare and present the file importer, it’s time to meet some essential details regarding file handling.

Handling imported files

We can check the result value and proceed to file handling either in the block of the completion handler, or in a separate method like the next one:

The usual way to “read” result, a Result value, is using a switch statement:

There’s not much to do in the failure case here, but in a real app this should be treated properly.

The interesting part is the success case, where the collection of URL objects is given as argument (urls). Since we need to process multiple files, we’ll get started with an iteration:

Here it comes the essential detail we should not skip. Selected files reside out of the app’s sandbox, and they are not accessible by default. Before trying to open or process each file, we need to:

  • Request for access to the file through the URL object; on success, the system grants our app with a temporary security scope so we can read each file.
  • Do any processing necessary.
  • End the access by relinquishing the security scope.

The above steps are represented in code right next:

The guard statement ensures that the file is not processed if access is not granted. The startAccessingSecurityScopedResource() and stopAccessingSecurityScopedResource(), methods available through the url object, start and stop access to the file respectively. I’d recommend to check out the documentation for more information.

Note:
If necessary, you can copy files from their original URL to a local folder in the app’s sandbox.

The handleImportedFiles(result:) is now complete:

An additional tip

We called stopAccessingSecurityScopedResource() method after the file processing is done in readFile(at:). That’s fine here, but if you have more complex code that throws errors or returns early, you should make sure that stopAccessingSecurityScopedResource() is called in all cases, otherwise the granted security scope is not relinquished.

Probably, it would be more practical to call it using defer like so:

Reading and displaying file contents

This part aims to make this example complete by adding the missing pieces in the presented code. The way to process files once access is granted depends on the requirements of each app. Here, the purpose is to read and show the contents of the imported text files. To keep things clear, we’ll keep two pieces of data; the file name and the content. We can use a custom type to keep them:

Instances of the above type will be presented in a List, so conforming to Identifiable protocol is necessary.

Additionally, we also need an ImportedFile collection, so we can store the data we want:

Now, we can go ahead and implement the readFile(at:) method that we called previously in handleImportedFiles(result:). It reads the file contents as a Data object, tries to convert it into a String value, and then creates a new ImportedFile instance which is appended into the importedFiles array:


Lastly, and with all the above in place, let’s display all read file contents in a List. The datasource is the importedFiles array:

Wrapping up

Most probably, fileImporter is not a well-known modifier, but it’s a great built-in mechanism to present a system-provided interface to pick files out of the app’s sandbox. Configuring and presenting it is a fairly simple task, as you’ve seen in the previous parts. But it’s crucial to remember requesting access before processing any files, and then stopping it. I hope you found this post valuable. Thanks for reading!


Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.