3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SwiftUI 2.0: How to import files into your iOS App

Last updated at Posted at 2021-04-23

The problem

I wanted to import audio files in an iOS 14.0+ app that I build for work using SwiftUI 2.0, but I ran into trouble with __security-scoped URL__s and styling, and decided to share my experience ...

File Picker

With the release of iOS 14 beta 6, Apple has provided us with a new view modifier enabling users to import an existing file from their iOS devices or their iCloud Drive. First, let's review this new fileImporter() modifier ...

The fileImporter modifier

The fileImporter(isPresented:allowedContentTypes:allowsMultipleSelection:onCompletion:) method allows users to import one or more files.

Declaration
SwiftUI
func fileImporter(
    isPresented: Binding<Bool>, 
    allowedContentTypes: [UTType], 
    allowsMultipleSelection: Bool, 
    onCompletion: @escaping (Result<[URL], Error>) -> Void
) -> some View
Parameters
  • isPresented: A binding to whether the file picker interface should be shown.
  • allowedContentTypes: The list of supported content types which can be imported.
  • allowsMultipleSelection: A boolean indicating wether users can select and import multiple files at the same time.
  • onCompletion: A callback that will be invoked when the operation has succeeded.
How it works
  • Set the variable binded by isPresented to true to display the file picker.
  • When the files to import have been selected, isPresented will be automatically set back to false before the onCompletion callback is called.
  • If the user cancels the import, isPresented will be automatically set back to false and the onCompletion callback will not called.
  • When the onCompletion callback is called, the result is either a .success or a .failure.
  • In case of .success, result will contain the list of __URL__s for the files to import.
Note

While allowedContentTypes can be modified at the same time the file importer is presented, it will have no immediate effect and will only apply the next time the file importer is presented.

Example Code

Let's import one audio file and play it to test our implementation:

SwiftUI
import SwiftUI
import AVFoundation
...
    @State private var isImporting = false
    var body: some View {
        Button(action: {
            isImporting = true
        }) {
        Image(systemName: "waveform.circle.fill")
            .font(.system(size: 40))
        }
        .fileImporter(
            isPresented: $isImporting,
            allowedContentTypes: [.audio],
            allowsMultipleSelection: false
        ) { result in
            if case .success = result {
                do {
                    let audioURL: URL = try result.get().first!

                    let audioPlayer = try AVAudioPlayer(contentsOf: audioURL)
                    audioPlayer.delegate = ...
                    audioPlayer.prepareToPlay()
                    audioPlayer.play() // ← ERROR raised here

                } catch {
                    let nsError = error as NSError
                    fatalError("File Import Error \(nsError), \(nsError.userInfo)")
                }
            } else {
                print("File Import Failed")
            }
        }
    }
...

What happened?!

The Error

This is the error I got in the XCode console when I ran my real implementation:

XCode
2021-04-23 10:00:01.123456+0900 MyApp[10691:1234567] Could not signal service com.apple.WebKit.WebContent: 113: Could not find specified service
2021-04-23 10:00:01.123456+0900 MyApp[10691:1234567] Could not signal service com.apple.WebKit.WebContent: 113: Could not find specified service
Playback initialization for "file:///private/var/mobile/Containers/Shared/AppGroup/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/File%20Provider%20Storage/Voice/TestFile.m4a" file has failed: Error Domain=NSOSStatusErrorDomain Code=-54 "(null)".
Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value: file MyApp/AudioPlayer.swift, line 123

What does this mean?

It means that apps on iOS are sandboxed, they have unlimited access to the files inside their sandbox but cannot access files outside their sandbox without the proper permissions.

How can we get the permission to access files outside the app sandbox?

Security Permissions

Once you get the list of __URL__s for the files to import, you have to get the permissions to access them.

The startAccessingSecurityScopedResource() method

Declaration
Swift
func startAccessingSecurityScopedResource() -> Bool
Return value

The function returns true if the request to access the file succeeded and false otherwise.

How it works

When you obtain a security-scoped URL, you cannot immediately use the file it points to.

To gain access to the file, you must call the startAccessingSecurityScopedResource() method (or its Core Foundation equivalent, the CFURLStartAccessingSecurityScopedResource(_:) function) on the security-scoped URL, which adds the file location to your app’s sandbox, thus making the file available to your app.

After successfully gaining access to a file, you must relinquish access to the file as soon as you are done with it.

To do that, call the stopAccessingSecurityScopedResource() method (or its Core Foundation equivalent, the CFURLStopAccessingSecurityScopedResource(_:) function) on the URL to relinquish access, and immediately lose access to the file.

Warning

If you fail to relinquish your access to file-system resources when you no longer need them, your app leaks kernel resources. If sufficient kernel resources are leaked, your app loses its ability to add file-system locations to its sandbox until relaunched.

Example Code
SwiftUI
...
    @State var audioFiles: Array<MediaFileObject> = Array<MediaFileObject>()
    @State private var isImporting = false
    var body: some View {
        Button(action: {
            isImporting = true
        }) {
        Image(systemName: "waveform.circle.fill")
            .font(.system(size: 40))
        }
        .fileImporter(
            isPresented: $isImporting,
            allowedContentTypes: [.audio],
            allowsMultipleSelection: false
        ) { result in
            if case .success = result {
                do {
                    let audioURL: URL = try result.get().first!
                    if audioURL.startAccessingSecurityScopedResource() {
                        audioFiles.append(AudioObject(id: UUID().uuidString, url: audioURL))
                    }
                } catch {
                    let nsError = error as NSError
                    fatalError("File Import Error \(nsError), \(nsError.userInfo)")
                }
            } else {
                print("File Import Failed")
            }
        }
    }
...

File Types

You can use common system data types for resources that you load, save, or open from other apps. Or you can define your own file and data types if necessary.

In my code I used the .audio type which represents all kind of audio files and belongs to the "Image, Audio, and Video Base Types" category:

  • image: A base type that represents image data.
  • audio: A type that represents audio that doesn’t contain video.
  • audiovisualContent: A base type that represents data that contains video content that may or may not also include audio.
  • movie: A base type representing media formats that may contain both video and audio.
  • video: A type that represents video that doesn’t contain audio.

Styling

Styling is still not really straightforward in SwiftUI 2.0 (hoping for SwiftUI 3.0) but I found a few ways to change the file picker appearance:

  • UINavigationBar.appearance().tintColor: UIColor: Is the color of the toolbar items in the file picker's header.
  • UITabBar.appearance().barTintColor: UIColor: Is the background color of the tabbar at the bottom of the file picker.
  • UITabBar.appearance().tintColor: UIColor: Is the color of the currently selected tabbar item.
  • UITabBar.appearance().unselectedItemTintColor: UIColor: Is the color of the currently not selected toolbar items.

Be careful ! As using UIKit settings this way can affect the appearance of navigation bars and tab bars in other parts of your application.

Last Words

I hope this article will help you to implement file import in your app.

The SwiftUI 1.0 way?

I tried to implement a file picker to import audio files another way (without fileImporter()), but I could not make it work!

The fileExporter modifier

The fileExporter(isPresented:document:contentType:defaultFilename:onCompletion:) method allows users to export an in-memory document to a file on disk.

The fileMover modifier

The fileMover(isPresented:file:onCompletion:) method allows users to move an existing file to a new location.

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?