iOS 13以降でUIImagePickerControllerが返すmediaURLの値が変わった謎回

# image picker controllerで取得したローカルファイルのURLがiOSのversionによって違うために生じるエラーを解消した経験より
畑田です。  
ローカルのフォトライブラリを取得するのによく用いる`UIImagePickerController`ですが、これが、delegate関数の引数として渡してくれるローカルファイルのURLがiOSのバージョンによって違うことがわかりました。  
さらに、自分はこのURLからFirebaseのSDKを用いてCloud Storage for Firebaseに保存しようとしたのですが、最新のiOSのバージョンの返すURLではエラーを吐いてしまいました。  
これを日本語で論じている文献が見当たらなかったので、記録しておきます。  
全てコードで書いています。

## 環境
- Swift version 5.3.2
- Xcode version 12.0.0

## 問題
そもそもの問題点を明らかにしておきます。  
`UIImagePickerController`にはdelegateメソッドが用意されており、`imagePickerController(_:didFinishPickingMediaWithInfo:)`はimage picker controllerのchooseボタンやsaveボタンを押したときに呼ばれるメソッドで、image picker controllerをmodallyに表示した状態からdismissする処理などはここで書きます。  
このメソッドの引数には`info`というlabelで`[UIImagePickerController.InfoKey : Any]`というtypeのdictionaryが渡されており、これをメソッドの中で参照することで選んだ画像や動画の情報を得ることができます。  
例えば、取得したいファイルのローカルのURLは`info[UIImagePickerController.InfoKey.mediaURL]`の値として取得できます。ちなみにFoundationではローカルファイルの所在も`URL`で表現します。  
この問題が生じるまでの我々のコードは以下のようでした。

```swift
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    // get a URL for the selected local file with nil safety
    guard let mediaURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL else { return }

    // assign the URL to the global variable
    self.mediaURL = mediaURL
    
    // instantiate the asset with the URL and pass it to the global variable
    asset = AVURLAsset(url: mediaURL)
    
    // dismiss the picker
    self.dismiss(animated: true, completion: nil)
}
```
以上で作ったmediaURLでCloud Storage for Firebaseに保存するコードが以下です。先のコードが書いてあるクラスとは別のクラスに値渡しをした後のコードなのでnil safetyを噛ませています。

```swift
private func save() {
    let storageRef = Storage.storage().reference()
    
    let uuid = NSUUID().uuidString
    let productMovieRef = storageRef.child("movies/products/\(uuid).mov")
    guard let mediaURL = mediaURL else { return }
    let uploadTask = productMovieRef.putFile(from: mediaURL as URL, metadata: nil) { metadata, putFileError in
        if let _ = putFileError { return print("put file error:", putFileError!) }
        // skip details
    }
    // skip details
}
```
このコードの`putFile`メソッドにおいてFirebaseがエラーコード`-13000`を返すようになったのです。`-13000`というのは`An unknown error occurred.`です。  
URLの内容をprintしてみたところ、`file:///private/var/mobile/Containers/Data/PluginKitPlugin/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/tmp/trim.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.MOV`が出力され、異変に気づき、<a href="https://github.com/apache/cordova-plugin-camera/issues/506">こちら</a>のような記事で報告があったので現状を理解しました。  
というのも、普段は`file:///var/mobile/Containers/Data/Application/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/tmp/trim.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.MOV`というような形のURLが返されるからです。  
結局のところ、`info[UIImagePickerController.InfoKey.mediaURL]`の値がiOSのバージョンによって変化し、外部SDKが当該ファイルを読み込めないようになっていることあるということです。  
iOS 13以降で生じ得る現象です。

## 解決策
今回の解決策として、SDKから取得できそうな、`~/tmp/`の中に取得したいファイルをコピーしてからそのURLをFirebaseのSDKに渡すように修正することにしました。  
具体的には、`imagePickerController(_:didFinishPickingMediaWithInfo:)`の中で`FileManager`を使ってファイルをいじっています。  
以下ソースコードです。

```swift
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    guard let mediaURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL else { return }
    
    // If the user's iOS version is 13 or later, `info[UIImagePickerController.InfoKey.mediaURL]` returns an invalid URL for uploading to cloud storage.
    if #available(iOS 13, *) {
        do {
            let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent("uploads", isDirectory: true).appendingPathComponent(mediaURL.lastPathComponent, isDirectory: false) // .../tmp/uploads/xxxxxx.mov
            try FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
            if FileManager.default.fileExists(atPath: destinationURL.path) {
                try FileManager.default.removeItem(at: destinationURL)
            }
            try FileManager.default.copyItem(at: mediaURL, to: destinationURL)
            self.mediaURL = destinationURL
        } catch {
            print(error)
        }
    } else {
        self.mediaURL = mediaURL
    }
    
    asset = AVURLAsset(url: mediaURL)
    
    self.dismiss(animated: true, completion: nil)
}
```
ここで注意すべきなのは、`copyItem(at:to:)`の`to`に対して、存在しないディレクトリを含むURLを指定するとエラーを吐くことと既に存在するファイルのURLを指定すると上書きできずエラーを吐くことです。  
上のソースコードでは、指定したURL(`destinationURL`)に対して`createDirectory(at:withIntermediateDirectories:attributes:)`でディレクトリを作成し、`fileExists(at:)`でファイルが既に存在したら、削除をする処理をコピーの前に噛ませています。
以上です。