From a2879ce270222ff8c2cd660b8b8eec1b14b731bf Mon Sep 17 00:00:00 2001 From: etoledom Date: Thu, 26 Sep 2024 10:56:27 +0200 Subject: [PATCH] Using PHPicker (#425) --- .../SystemImagePicker/CameraImagePicker.swift | 51 ++++++++++++++ .../SystemImagePicker/PhotosImagePicker.swift | 70 +++++++++++++++++++ .../SystemImagePickerView.swift | 67 ++---------------- 3 files changed, 127 insertions(+), 61 deletions(-) create mode 100644 Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/CameraImagePicker.swift create mode 100644 Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/PhotosImagePicker.swift rename Sources/GravatarUI/SwiftUI/AvatarPicker/{ => SystemImagePicker}/SystemImagePickerView.swift (60%) diff --git a/Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/CameraImagePicker.swift b/Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/CameraImagePicker.swift new file mode 100644 index 00000000..c60524ff --- /dev/null +++ b/Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/CameraImagePicker.swift @@ -0,0 +1,51 @@ +import PhotosUI +import SwiftUI + +struct CameraImagePicker: UIViewControllerRepresentable { + let onImageSelected: (ImagePickerItem) -> Void + + @State private var pickedImage: ImagePickerItem? { + didSet { + if let pickedImage { + onImageSelected(pickedImage) + } + } + } + + func makeUIViewController(context: Context) -> UIImagePickerController { + let imagePicker = UIImagePickerController() + // Forcefully use UIImagePickerController for device camera only + imagePicker.sourceType = .camera + imagePicker.delegate = context.coordinator + // Use custom image cropper + imagePicker.allowsEditing = false + + return imagePicker + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { + // No-op + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + var parent: CameraImagePicker + + init(_ parent: CameraImagePicker) { + self.parent = parent + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + parent.pickedImage = .init(id: UUID().uuidString, image: image) + } + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true) + } + } +} diff --git a/Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/PhotosImagePicker.swift b/Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/PhotosImagePicker.swift new file mode 100644 index 00000000..1da49da2 --- /dev/null +++ b/Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/PhotosImagePicker.swift @@ -0,0 +1,70 @@ +import PhotosUI +import SwiftUI + +struct PhotosImagePicker: UIViewControllerRepresentable { + let onImageSelected: (ImagePickerItem) -> Void + let onCancel: () -> Void + + @State private var pickedImage: ImagePickerItem? { + didSet { + if let pickedImage { + onImageSelected(pickedImage) + } + } + } + + func makeUIViewController(context: Context) -> PHPickerViewController { + var config = PHPickerConfiguration() + config.filter = .images + config.selectionLimit = 1 + let picker = PHPickerViewController(configuration: config) + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) { + // No-op + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + final class Coordinator: NSObject, PHPickerViewControllerDelegate { + let parent: PhotosImagePicker + + init(_ parent: PhotosImagePicker) { + self.parent = parent + } + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + guard + let result = results.first, + result.itemProvider.canLoadObject(ofClass: UIImage.self) + else { + parent.onCancel() + return + } + + Task { + let image = await result.itemProvider.loadUIImage() + let imageItem = ImagePickerItem(id: UUID().uuidString, image: image) + parent.pickedImage = imageItem + } + } + } +} + +extension NSItemProvider { + fileprivate func loadUIImage() async -> UIImage { + await withCheckedContinuation { continuation in + loadObject(ofClass: UIImage.self) { itemReading, _ in + guard let image = itemReading as? UIImage else { + return + } + + continuation.resume(returning: image) + } + } + } +} diff --git a/Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePickerView.swift b/Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/SystemImagePickerView.swift similarity index 60% rename from Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePickerView.swift rename to Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/SystemImagePickerView.swift index 75e6c71d..3ba7220a 100644 --- a/Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePickerView.swift +++ b/Sources/GravatarUI/SwiftUI/AvatarPicker/SystemImagePicker/SystemImagePickerView.swift @@ -7,10 +7,6 @@ struct SystemImagePickerView: View where La let onImageSelected: (UIImage) -> Void var body: some View { - // NOTE: Here we can choose between legacy and new picker. - // So far, the new SwiftUI PhotosPicker only supports Photos library, no camera, and no cropping, so we are only using legacy for now. - // The interface (using a Label property as the element to open the picker) is the same as in the new SwiftUI picker, - // which will make it easy to change it later on. ImagePicker(label: label, onImageSelected: onImageSelected, customEditor: customEditor) } } @@ -79,13 +75,15 @@ private struct ImagePicker: View where Labe case .camera: ZStack { Color.black.ignoresSafeArea(edges: .all) - LegacyImagePickerRepresentable(sourceType: source.map()) { item in + CameraImagePicker { item in pickerDidSelectImage(item) } } case .photoLibrary: - LegacyImagePickerRepresentable(sourceType: source.map()) { item in + PhotosImagePicker { item in pickerDidSelectImage(item) + } onCancel: { + sourceType = nil }.ignoresSafeArea() } } @@ -130,60 +128,7 @@ extension ImagePicker.SourceType { } } -struct LegacyImagePickerRepresentable: UIViewControllerRepresentable { - @Environment(\.presentationMode) private var presentationMode - - var sourceType: UIImagePickerController.SourceType - let onImageSelected: (ImagePickerItem) -> Void - - @State private var pickedImage: ImagePickerItem? { - didSet { - if let pickedImage { - onImageSelected(pickedImage) - } - } - } - - func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController { - let imagePicker = UIImagePickerController() - imagePicker.sourceType = sourceType - imagePicker.delegate = context.coordinator - - return imagePicker - } - - func updateUIViewController( - _ uiViewController: UIImagePickerController, - context: UIViewControllerRepresentableContext - ) {} - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { - var parent: LegacyImagePickerRepresentable - - init(_ parent: LegacyImagePickerRepresentable) { - self.parent = parent - } - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - guard let url = info[UIImagePickerController.InfoKey.imageURL] as? URL else { return } - if picker.allowsEditing, let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { - parent.pickedImage = .init(image: image, url: url) - } else if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { - parent.pickedImage = .init(image: image, url: url) - } - } - } -} - -struct ImagePickerItem: Identifiable { - var id: String { - url.absoluteString - } - +struct ImagePickerItem: Identifiable, Sendable { + let id: String let image: UIImage - let url: URL }