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
で表現します。
この問題が生じるまでの我々のコードは以下のようでした。
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を噛ませています。
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
が出力され、異変に気づき、こちらのような記事で報告があったので現状を理解しました。
というのも、普段は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
を使ってファイルをいじっています。
以下ソースコードです。
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:)
でファイルが既に存在したら、削除をする処理をコピーの前に噛ませています。
以上です。