🎃

PHPickerViewControllerの処理をSwift Concurrencyを使って書いた

2023/02/11に公開

PHPickerViewControllerの処理をSwift Concurrencyを使って書いた。それの備忘録。
SwiftUI版で書いてるが、UIKitでも同様に書ける。

完成したもの

struct ImagePicker: UIViewControllerRepresentable {
    @Environment(\.dismiss) fileprivate var dismiss
    let select: ([UIImage]) -> Void

    func makeUIViewController(context: Context) -> PHPickerViewController {
        var configuration = PHPickerConfiguration()
        configuration.filter = PHPickerFilter.images
        configuration.selectionLimit = 0 // maximum
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ picker: UIViewControllerType, context: Context) {
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        private var parent: ImagePicker

        init(_ picker: ImagePicker) {
            self.parent = picker
        }

        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            Task {
                let images = await withThrowingTaskGroup(of: UIImage.self) { group in
                    for result in results {
                        group.addTask { try await self.loadImage(result: result) }
                    }
                    var images = [UIImage]()
                    while let nextResult = await group.nextResult() {
                        switch nextResult {
                        case .success(let value):
                            images.append(value)
                        case .failure(let error):
                            print(error)
                        }
                    }
                    return images
                }
                parent.select(images)
                parent.dismiss()
            }
        }

        private func loadImage(result: PHPickerResult) async throws -> UIImage {
            return try await withCheckedThrowingContinuation { continuation in
                let provider = result.itemProvider
                provider.loadObject(ofClass: UIImage.self) { image, error in
                    if let error {
                        continuation.resume(throwing: error)
                        return
                    }
                    guard let image = image as? UIImage else {
                        continuation.resume(throwing: PickerError.missingImage)
                        return
                    }
                    continuation.resume(returning: image)
                }
            }
        }
    }
}
private enum PickerError: Error {
    case missingImage
}

errorの扱い方

上記のコードではfunc loadImageでerrorがthrowされても、他のUIImageだけで成功するようにnextResultを使って処理を書いたが、1つerrorが出たら終了させて良いのであれば、下記のように書ける。

- let images = await withThrowingTaskGroup(of: UIImage.self) { group in
+ let images = try await withThrowingTaskGroup(of: UIImage.self) { group in
    for result in results {
        group.addTask { try await self.loadImage(result: result) }
    }
-    var images = [UIImage]()
-    while let nextResult = await group.nextResult() {
-        switch nextResult {
-        case .success(let value):
-            images.append(value)
-        case .failure(let error):
-            print(error)
-        }
-    }
-    return images
+    return try await group.reduce(into: [UIImage]()) { $0.append($1) }
}

今回はfunc pickerでerror処理をしたかったのでこのように書いたが、親ViewでAlertを表示させたいなどの場合は違う書き方となる。

また、loadObjectでerrorがnilでimageをUIImageでキャストできないケースがどういう時かわからなかったが、一応PickerError.missingImageを流しておいた。

if let error {
    continuation.resume(throwing: error)
    return
}
guard let image = image as? UIImage else {
    continuation.resume(throwing: PickerError.missingImage)
    return
}
continuation.resume(returning: image)

Discussion