[PhotoPicker]SwiftUIでasync/awaitに対応させたPHPickerViewControllerを使う
目標
SwiftUIでPHPickerViewControllerを綺麗に使う
問題点
NSItemProvider
のloadFileRepresentation(forTypeIdentifier:)
とloadObject(ofClass:)
がasync/awaitに対応していない
やってもダメだったこと
loadFileRepresentation(loadFileRepresentation:)
がcompletionHandlerを抜けるとファイルパスを削除してしまうため、loadItem(forTypeIdentifier: options) async throws -> NSSecureCoding`の使用を試みたが、なぜか一部の動画が取得できなかった為、不採用に
原因は分かりませんでした
loadFilePresentation, loadObjectをasync/awaitへ対応
extension NSItemProvider {
public func loadFileRepresentation(forTypeIdentifier typeIdentifier: String) async throws -> URL {
try await withCheckedThrowingContinuation { continuation in
self.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in
if let error = error {
return continuation.resume(throwing: error)
}
guard let url = url else {
return continuation.resume(throwing: NSError())
}
let localURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
try? FileManager.default.removeItem(at: localURL)
do {
try FileManager.default.copyItem(at: url, to: localURL)
} catch {
return continuation.resume(throwing: error)
}
continuation.resume(returning: localURL)
}.resume()
}
}
public func loadObject(ofClass aClass : NSItemProviderReading.Type) async throws -> NSItemProviderReading {
try await withCheckedThrowingContinuation { continuation in
self.loadObject(ofClass: aClass) { data, error in
if let error = error {
return continuation.resume(throwing: error)
}
guard let data = data else {
return continuation.resume(throwing: NSError())
}
continuation.resume(returning: data)
}.resume()
}
}
}
PHLivePhotoViewをSwiftUIで使えるように
struct LivePhoto: UIViewRepresentable {
let livePhoto: PHLivePhoto
func makeUIView(context: Context) -> PHLivePhotoView {
let livePhotoView = PHLivePhotoView()
livePhotoView.livePhoto = livePhoto
return livePhotoView
}
func updateUIView(_ livePhotoView: PHLivePhotoView, context: Context) {
}
}
小さいモデルを用意
assetIdentifier
, NSItemProvider
, NSItemProviderReading(UIImage, PHLivePhoto, URL)
をもつモデルを用意
struct PhotoResult {
let id: String
let provider: NSItemProvider
var item: NSItemProviderReading?
}
PhotoPickerを用意
struct PhotoPicker: UIViewControllerRepresentable {
@Binding public var results: [PhotoResult]
@Binding public var didPickPhoto: Bool
init(results: Binding<[PhotoResult]>, didPickPhoto: Binding<Bool>) {
self._results = results
self._didPickPhoto = didPickPhoto
}
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration(photoLibrary: .shared())
configuration.preselectedAssetIdentifiers = results.map { $0.id }
configuration.selectionLimit = 0
configuration.preferredAssetRepresentationMode = .current
configuration.selection = .ordered
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate, UINavigationControllerDelegate {
var parent: PhotoPicker
init(_ parent: PhotoPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
let existingSelection = parent.results
var newResults: [PhotoResult] = []
for result in results {
let id = result.assetIdentifier!
let firstItem = existingSelection.first(where: { $0.id == id })
let provider = firstItem?.provider ?? result.itemProvider
let result: PhotoResult = .init(id: id, provider: provider, item: firstItem?.item)
newResults.append(result)
}
parent.results = newResults
parent.didPickPhoto = true
}
}
}
PhotoViewを用意
PhotoViewは通常の画像、Live Photo、タップしたら動画(VideoPlayer)にとぶものの3つのどれかを表示することができるSwiftUI用のViewです。
struct PhotoView: View {
let provider: NSItemProvider
@Binding var item: NSItemProviderReading?
@State var tappedImage: UIImage?
@State var isPresentedVideoPlayer = false
var body: some View {
GeometryReader { geometry in
if let item = item {
if let livePhoto = item as? PHLivePhoto {
LivePhoto(livePhoto: livePhoto)
}
else if let uiimage = item as? UIImage {
Image(uiImage: uiimage)
.resizable()
.aspectRatio(contentMode: .fit)
} else if let url = item as? URL {
ZStack {
if let uiImage = try? UIImage(movieURL: url) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
}
Image(systemName: "play")
}
.onTapGesture {
isPresentedVideoPlayer.toggle()
}
.sheet(isPresented: $isPresentedVideoPlayer) {
let player: AVPlayer = .init(url: url)
VideoPlayer(player: player)
.onAppear {
player.play()
}
}
}
}
}
.onAppear {
Task {
if item == nil {
do {
item = try await provider.loadPhoto()
} catch {
print(error)
}
}
}
}
}
}
extension NSItemProvider {
public func loadPhoto() async throws -> NSItemProviderReading {
if self.canLoadObject(ofClass: PHLivePhoto.self) {
return try await self.loadObject(ofClass: PHLivePhoto.self)
}
else if self.canLoadObject(ofClass: UIImage.self) {
return try await self.loadObject(ofClass: UIImage.self)
}
else if self.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
let url = try await self.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier)
return url as NSItemProviderReading
}
fatalError()
}
}
extension UIImage {
public convenience init(movieURL url: URL) throws {
let asset: AVAsset = .init(url: url)
let generator = AVAssetImageGenerator(asset: asset)
let cgImage = try generator.copyCGImage(at: asset.duration, actualTime: nil)
self.init(cgImage: cgImage)
}
}
主画面
struct ContentView: View {
@State var results: [PhotoResult] = []
@State var isPresentedPhotoPicker = false
@State var didPickPhoto = true
var body: some View {
VStack {
Button(action: {
isPresentedPhotoPicker = true
}, label: {
Image(systemName: "photo")
})
.padding()
.disabled(!didPickPhoto)
.sheet(isPresented: $isPresentedPhotoPicker) {
PhotoPicker(results: $results, didPickPhoto: $didPickPhoto)
}
ScrollView(.vertical) {
ForEach(0..<results.count, id: \.self) { i in
PhotoView(provider: results[i].provider, item: .init(get: { results[i].item }, set: { results[i].item = $0 }))
.frame(height: 300)
}
}
}
}
}
つまづいた点
- 画像を選択した順序を保持
- loadFilePresentationをloadItemに置き換えてもダメだった
参考にしたもの
公式のPHPickerViewController(UIKit)のサンプル
Author And Source
この問題について([PhotoPicker]SwiftUIでasync/awaitに対応させたPHPickerViewControllerを使う), 我々は、より多くの情報をここで見つけました https://zenn.dev/zunda_pixel/articles/00f149787bed86著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol