SwiftUI 2.0: How to import files into your iOS App

## 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()`](https://developer.apple.com/documentation/mapkit/map/3679199-fileimporter) modifier ...
### The [`fileImporter`](https://developer.apple.com/documentation/mapkit/map/3679199-fileimporter) modifier
The [`fileImporter(isPresented:allowedContentTypes:allowsMultipleSelection:onCompletion:)`](https://developer.apple.com/documentation/mapkit/map/3679199-fileimporter) method allows users to import one or more files.
##### Declaration
```Swift: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](https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers) 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:
 
```Swift: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: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()`](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso) method
##### Declaration
```Swift: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()`](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso) method (or its Core Foundation equivalent, the [`CFURLStartAccessingSecurityScopedResource(_:)`](https://developer.apple.com/documentation/corefoundation/1543318-cfurlstartaccessingsecurityscope) 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()`](https://developer.apple.com/documentation/foundation/nsurl/1413736-stopaccessingsecurityscopedresou) method (or its Core Foundation equivalent, the [`CFURLStopAccessingSecurityScopedResource(_:)`](https://developer.apple.com/documentation/corefoundation/1542381-cfurlstopaccessingsecurityscoped) 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
```Swift: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](https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers) for resources that you load, save, or open from other apps. Or you can [define your own file and data types](https://developer.apple.com/documentation/uniformtypeidentifiers/defining_file_and_data_types_for_your_app) 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](https://qiita.com/loic-meister-guild/questions/ea965773de8cc2b691bf) (without `fileImporter()`), but I could not make it work!
### The [`fileExporter`](https://developer.apple.com/documentation/swiftui/form/fileexporter(ispresented:document:contenttype:defaultfilename:oncompletion:)-88n4r) modifier
The [`fileExporter(isPresented:document:contentType:defaultFilename:onCompletion:)`](https://developer.apple.com/documentation/swiftui/form/fileexporter(ispresented:document:contenttype:defaultfilename:oncompletion:)-88n4r) method allows users to export an in-memory document to a file on disk.
### The [`fileMover`](https://developer.apple.com/documentation/swiftui/form/filemover(ispresented:file:oncompletion:)) modifier
The [`fileMover(isPresented:file:onCompletion:)`](https://developer.apple.com/documentation/swiftui/form/filemover(ispresented:file:oncompletion:)) method allows users to move an existing file to a new location.