0
1

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.

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

Last updated at Posted at 2021-03-18

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:)でファイルが既に存在したら、削除をする処理をコピーの前に噛ませています。
以上です。

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?