PHPickerViewControllerから選択した画像を取得する為にNSItemProviderのloadObject(ofClass:completionHandler:)
を使用するのですが、completionHandlerをスッキリさせる為にasync/awaitに対応してみるとクロージャーの中が呼ばれなくなって困りました。
今回はその対応について書き残したいと思います。
環境
- Xcode 14.1
- iOS 16.1
PHPickerViewControllerの構成
選択できる写真は5枚、アセットタイプは画像にしています。
import UIKit
import PhotosUI
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var configuration = PHPickerConfiguration()
configuration.selectionLimit = 5
configuration.filter = .images
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
present(picker, animated: true)
}
}
loadObject
通常ケース
画像を選択後、PHPickerViewControllerが閉じて、取得した結果をImageに変換する処理です。
// MARK: - 通常
extension ViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
let itemProviders = results.compactMap { $0.itemProvider }.filter { $0.canLoadObject(ofClass: UIImage.self)}
if itemProviders.isEmpty { return }
itemProviders.forEach { itemProvider in
itemProvider.loadObject(ofClass: UIImage.self) { item, error in
if let error {
print("Failure to get Image with", error)
return
}
if let image = item as? UIImage {
print("Image:", image)
return
}
print("Failure to get Image")
}
}
}
}
実際に写真を5枚選択すると、デバッグコンソールに5回分のUIImage
取得のログが表示されました。
Image: <UIImage:0x2818a62e0 anonymous {3024, 4032} renderingMode=automatic>
Image: <UIImage:0x2818aea30 anonymous {3024, 4032} renderingMode=automatic>
Image: <UIImage:0x2818ae9a0 anonymous {3024, 4032} renderingMode=automatic>
Image: <UIImage:0x2818a6400 anonymous {3024, 4032} renderingMode=automatic>
Image: <UIImage:0x2818a62e0 anonymous {3024, 4032} renderingMode=automatic>
async/await対応ケース
エクステンションの作成
まずは、async/awaitに対応したNSItemProviderのエクステンションを作成します。
extension NSItemProvider {
func loadObject(ofClass aClass : NSItemProviderReading.Type) async throws -> NSItemProviderReading {
try await withCheckedThrowingContinuation { continuation in
self.loadObject(ofClass: aClass) { item, error in
if let error {
return continuation.resume(throwing: error)
}
guard let item else {
return continuation.resume(throwing: NSError())
}
continuation.resume(returning: item)
}
}
}
}
エラーがなければ、NSItemProviderReading
を返してくれます。
loadObjectを使用
// MARK: - async/await対応
extension ViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
let itemProviders = results.compactMap { $0.itemProvider }.filter { $0.canLoadObject(ofClass: UIImage.self)}
if itemProviders.isEmpty { return }
itemProviders.forEach { itemProvider in
Task {
do {
let item = try await itemProvider.loadObject(ofClass: UIImage.self)
if let image = item as? UIImage {
print("Image:", image)
} else {
print("Failure to get Image")
}
} catch {
print("Failure to get Image with", error)
}
}
}
}
}
通常のケースと同じように写真を5枚選択すると、デバッグコンソールに何も表示されませんでした。エラーすらも表示されていませんでした。
写真を1枚のみにして試してみると、UIImage
取得のログ出力が表示されました。エクステンションメソッドは機能しているようです。
Image: <UIImage:0x283006370 anonymous {3024, 4032} renderingMode=automatic>
2枚以上で試しても結果は何も出力されていませんでした。
エクステンションメソッドにログを埋め込む
開始前と開始後、終了時にログを埋め込んでみます。
extension NSItemProvider {
func loadObject(ofClass aClass : NSItemProviderReading.Type) async throws -> NSItemProviderReading {
try await withCheckedThrowingContinuation { continuation in
print("will start loading object")
self.loadObject(ofClass: aClass) { item, error in
print("did start loading object")
if let error {
print("end loading object")
return continuation.resume(throwing: error)
}
guard let item else {
print("end loading object")
return continuation.resume(throwing: NSError())
}
print("end loading object")
continuation.resume(returning: item)
}
}
}
}
同じように5枚の画像を選択して結果を見てみます。
will start loading object
will start loading object
2回だけwill start loading object
が呼ばれ、その後の出力は一切ありません、、。
ちなみに1枚選択だとしっかりログに出力されました。
will start loading object
did start loading object
end loading object
Image: <UIImage:0x281071b90 anonymous {3024, 4032} renderingMode=automatic>
原因
今回はforEach
後にTask
処理を実行していて、その処理が悪さをしていそうです。
対策
1枚だと成功するのでasync
対応したforEach
を作成し、Task
をforEach
の外側から囲み、順番に読み込むことにしました。
asyncForEach
extension Sequence {
func asyncForEach(_ operation: (Element) async throws -> Void) async rethrows {
for element in self {
try await operation(element)
}
}
}
対策後コード
// MARK: - async/await対応 - 修正
extension ViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
let itemProviders = results.compactMap { $0.itemProvider }.filter { $0.canLoadObject(ofClass: UIImage.self)}
if itemProviders.isEmpty { return }
Task {
await itemProviders.asyncForEach { itemProvider in
do {
let item = try await itemProvider.loadObject(ofClass: UIImage.self)
if let image = item as? UIImage {
print("Image:", image)
} else {
print("Failure to get Image")
}
} catch {
print("Failure to get Image with", error)
}
}
}
}
}
画像を5枚選択して、ログを確認します。
will start loading object
did start loading object
end loading object
Image: <UIImage:0x283875b90 anonymous {3024, 4032} renderingMode=automatic>
will start loading object
did start loading object
end loading object
Image: <UIImage:0x283875b90 anonymous {3024, 4032} renderingMode=automatic>
will start loading object
did start loading object
end loading object
Image: <UIImage:0x283875c20 anonymous {3024, 4032} renderingMode=automatic>
will start loading object
did start loading object
end loading object
Image: <UIImage:0x283875c20 anonymous {3024, 4032} renderingMode=automatic>
will start loading object
did start loading object
end loading object
Image: <UIImage:0x28387ad90 anonymous {3024, 4032} renderingMode=automatic>
5枚分のUIImage
取得のログを確認出来ました。
おわりに
せめて、エラーだけでも返してよという気持ちになりました。
今回はこのような対策になってしまいましたが、もっと良い対応策ありましたら優しく教えていただけると嬉しいです。