SwiftUI 2.0: iOSアプリにファイルをインポートする方法

## 課題
仕事でSwiftUI 2.0を使用して構築したiOS14.0以降のアプリに_audio_ファイルをインポートしたかったが、__security-scoped URL__sとスタイリングで問題が発生したことで、私の経験を共有したいと思った...
## ファイルピッカー
iOS 14ベータ6のリリースに伴い、Appleは、ユーザーが__iOSデバイス__または__iCloudドライブ__から既存のファイルをインポートできるようにする新しいビュー修飾子を提供した。まず、この新しい[`fileImporter()`](https://developer.apple.com/documentation/mapkit/map/3679199-fileimporter)修飾子を確認しましょう...
### [`fileImporter`](https://developer.apple.com/documentation/mapkit/map/3679199-fileimporter)修飾子
[`fileImporter(isPresented:allowedContentTypes:allowsMultipleSelection:onCompletion:)`](https://developer.apple.com/documentation/mapkit/map/3679199-fileimporter)メソッドを使用すると、ユーザーは1つ以上のファイルをインポートできる。
##### 宣言
```Swift:SwiftUI
func fileImporter(
    isPresented: Binding<Bool>, 
    allowedContentTypes: [UTType], 
    allowsMultipleSelection: Bool, 
    onCompletion: @escaping (Result<[URL], Error>) -> Void
) -> some View
```
##### パラメーター
- __isPresented__: ファイルピッカーインターフェイスを表示するかどうかを支持するバインディングです。
- __allowedContentTypes__: インポート可能なサポートされている[コンテンツタイプ](https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers)のリストです。
- __allowsMultipleSelection__: ユーザーが複数のファイルを同時に選択してインポートできるかどうかを支持するブール値です。
- __onCompletion__: 操作が成功したときに呼び出されるコールバックです。

##### 詳細
- ファイルピッカーを表示するには、 `isPresented`でバインドされている変数を__true__に設定する。
- インポートするファイルが選択されると、 `onCompletion`コールバックが呼び出される前に` isPresented`が自動的に__false__に戻される。
- ユーザーがインポートをキャンセルすると、 `isPresented`は自動的に__false__に戻され、` onCompletion`コールバックは呼び出されない。
- `onCompletion`コールバックが呼び出されると、` result`は `.success`または` .failure`のいずれかになる。
- `.success`の場合、 `result`には、インポートするファイルの__URL__sのリストが含まれる。

##### 注意
`allowedContentTypes`は、ファイルインポータが表示されると同時に変更できru
が、すぐには効果がなく、次にファイルインポータが表示されたときにのみ適用される。
##### サンプルコード
1つのオーディオファイルをインポートして再生することで、実装のコードをテストしてみましょう：
 
```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")
            }
        }
    }
...
```

何が起こった？！
### エラー
下記は、実際の実装のコードを実行したときにXCodeコンソールで発生したエラーです：

```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

```
##### エラーの意味
iOS上のアプリはサンドボックス化されており、__サンドボックス内__のファイルへのアクセスは__無制限__ですが、適切な__アクセス許可__がない時、__サンドボックス外__のファイルへのアクセスは__制限__される。

アプリのサンドボックス外のファイルにアクセスするためのアクセス許可を取得するにはどうすればよいですか？
## セキュリティ権限
インポートするファイルの__URL__のリストを取得したら、それらにアクセスするためのアクセス許可を取得する必要があります。
### [`startAccessingSecurityScopedResource()`](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso) メソッド
##### 宣言
```Swift:Swift
func startAccessingSecurityScopedResource() -> Bool
```
##### リターンバリュー
この関数は、ファイルへのアクセス要求が成功した場合は__true__を返し、それ以外の場合は__false__を返す。
##### 詳細
セキュリティスコープのURLを取得した場合、それが指すファイルをすぐに使用することはできない。

ファイルにアクセスするには、セキュリティスコープのURLで[`startAccessingSecurityScopedResource()`](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso)メソッド（またはCore Foundationの同等の[`CFURLStartAccessingSecurityScopedResource(_:)`](https://developer.apple.com/documentation/corefoundation/1543318-cfurlstartaccessingsecurityscope)関数）を呼び出す必要があり、これによりファイルの場所がアプリのサンドボックスに追加され、ファイルはアプリで利用可能になる。


ファイルへのアクセスを正常に取得した後、ファイルを使い終わったらすぐにファイルへのアクセスを放棄する必要があります。

そのために、URLで[`stopAccessingSecurityScopedResource()`](https://developer.apple.com/documentation/foundation/nsurl/1413736-stopaccessingsecurityscopedresou)メソッド（またはそれに相当するCore Foundationの同等の[`CFURLStopAccessingSecurityScopedResource(_:)`](https://developer.apple.com/documentation/corefoundation/1542381-cfurlstopaccessingsecurityscoped)関数）を呼び出すとアクセスを放棄し、すぐにファイルへのアクセスができなくなる。
##### 注意

ファイルシステムリソースが不要になったときにアクセスを放棄しなかった場合、アプリはカーネルリソースが漏れる。多くのカーネルリソースが漏れたら、アプリは再起動されるまでサンドボックスにファイルシステムの場所を追加できなくなる。
##### サンプルコード
```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")
            }
        }
    }
...
```

## ファイルタイプ
他のアプリから読み込む・保存する・開くリソースには、[共有のシステムデータ形式](https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers)を使用できる。または、必要に応じて[独自のファイルとデータ形式を定義](https://developer.apple.com/documentation/uniformtypeidentifiers/defining_file_and_data_types_for_your_app)できる。

私のコードでは、すべての種類のオーディオファイルを表し、「画像、オーディオ、およびビデオの基本タイプ」カテゴリに属する​​`.audio`タイプを使用した；

- __image__: 画像データを表す基本タイプ。
- __audio__: ビデオを含まないオーディオを表すタイプ。
- __audiovisualContent__: オーディオを含む場合と含まない場合があるビデオコンテンツを含むデータを表す基本タイプ。
- __movie__: ビデオとオーディオの両方を含む可能性のあるメディア形式を表す基本タイプ。
- __video__: 音声を含まないビデオを表すタイプ。

## スタイリング
SwiftUI 2.0（SwiftUI 3.0を期待）では、スタイリングはまだ実際には簡単ではないが、ファイルピッカーの外観を変更するいくつかの方法を見つけた：

- `UINavigationBar.appearance().tintColor: UIColor`: ファイルピッカーのヘッダーにあるツールバー項目の色です。
- `UITabBar.appearance().barTintColor: UIColor`: ファイルピッカーの下部にあるタブバーの背景色です。
- `UITabBar.appearance().tintColor: UIColor`: 現在選択されているタブバーアイテムの色です。
- `UITabBar.appearance().unselectedItemTintColor: UIColor`: 現在選択されていないツールバーアイテムの色です。

注意してください ！ UIKit設定をこのように使用すると、アプリケーションの他の部分のナビゲーションバーとタブバーの外観に影響を与える可能性がある。
## 最後に
この記事が、アプリにファイルのインポートを実装するのに役立つことを願っています。
### SwiftUI 1.0のやり方?
別の方法で（`fileImporter()`なし）[オーディオファイルをインポートするファイルピッカーを実装](https://qiita.com/loic-meister-guild/questions/ea965773de8cc2b691bf)しようとしたができなかった！
### [`fileExporter`](https://developer.apple.com/documentation/swiftui/form/fileexporter(ispresented:document:contenttype:defaultfilename:oncompletion:)-88n4r)修飾子
[`fileExporter(isPresented:document:contentType:defaultFilename:onCompletion:)`](https://developer.apple.com/documentation/swiftui/form/fileexporter(ispresented:document:contenttype:defaultfilename:oncompletion:)-88n4r)メソッドを使用すると、ユーザーはメモリ内のドキュメントをディスク上のファイルにエクスポートできる。
### [`fileMover`](https://developer.apple.com/documentation/swiftui/form/filemover(ispresented:file:oncompletion:))修飾子
[`fileMover(isPresented:file:onCompletion:)`](https://developer.apple.com/documentation/swiftui/form/filemover(ispresented:file:oncompletion:))メソッドを使用すると、ユーザーは既存のファイルを新しい場所に移動させることができる。