MediaPicker.swift (6006B)
1 // 2 // ImagePicker.swift 3 // damus 4 // 5 // Created by Swift on 3/31/23. 6 // 7 8 import UIKit 9 import SwiftUI 10 import PhotosUI 11 12 struct MediaPicker: UIViewControllerRepresentable { 13 14 @Environment(\.presentationMode) 15 @Binding private var presentationMode 16 17 @Binding var image_upload_confirm: Bool 18 var imagesOnly: Bool = false 19 let onMediaPicked: (PreUploadedMedia) -> Void 20 21 22 final class Coordinator: NSObject, PHPickerViewControllerDelegate { 23 let parent: MediaPicker 24 25 init(_ parent: MediaPicker) { 26 self.parent = parent 27 } 28 29 func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { 30 if results.isEmpty { 31 self.parent.presentationMode.dismiss() 32 } 33 34 for result in results { 35 if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { 36 result.itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (item, error) in 37 guard let url = item as? URL else { return } 38 39 if(url.pathExtension == "gif") { 40 // GIFs do not natively support location metadata (See https://superuser.com/a/556320 and https://www.w3.org/Graphics/GIF/spec-gif89a.txt) 41 // It is better to avoid any GPS data processing at all, as it can cause the image to be converted to JPEG. 42 // Therefore, we should load the file directtly and deliver it as "already processed". 43 44 // Load the data for the GIF image 45 // - Don't load it as an UIImage since that can only get exported into JPEG/PNG 46 // - Don't load it as a file representation because it gets deleted before the upload can occur 47 _ = result.itemProvider.loadDataRepresentation(for: .gif, completionHandler: { imageData, error in 48 guard let imageData else { return } 49 let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: "gif") 50 do { 51 try imageData.write(to: destinationURL) 52 Task { 53 await self.chooseMedia(.processed_image(destinationURL)) 54 } 55 } 56 catch { 57 Log.error("Failed to write GIF image data from Photo picker into a local copy", for: .image_uploading) 58 } 59 }) 60 } 61 else if canGetSourceTypeFromUrl(url: url) { 62 // Media was not taken from camera 63 self.attemptAcquireResourceAndChooseMedia( 64 url: url, 65 fallback: processImage, 66 unprocessedEnum: {.unprocessed_image($0)}, 67 processedEnum: {.processed_image($0)} 68 ) 69 } else { 70 // Media was taken from camera 71 result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in 72 if let image = image as? UIImage, error == nil { 73 self.chooseMedia(.uiimage(image)) 74 } 75 } 76 } 77 } 78 } else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { 79 result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { (url, error) in 80 guard let url, error == nil else { return } 81 82 self.attemptAcquireResourceAndChooseMedia( 83 url: url, 84 fallback: processVideo, 85 unprocessedEnum: {.unprocessed_video($0)}, 86 processedEnum: {.processed_video($0)} 87 ) 88 } 89 } 90 } 91 } 92 93 private func chooseMedia(_ media: PreUploadedMedia) { 94 self.parent.onMediaPicked(media) 95 self.parent.image_upload_confirm = true 96 } 97 98 private func attemptAcquireResourceAndChooseMedia(url: URL, fallback: (URL) -> URL?, unprocessedEnum: (URL) -> PreUploadedMedia, processedEnum: (URL) -> PreUploadedMedia) { 99 if url.startAccessingSecurityScopedResource() { 100 // Have permission from system to use url out of scope 101 print("Acquired permission to security scoped resource") 102 self.chooseMedia(unprocessedEnum(url)) 103 } else { 104 // Need to copy URL to non-security scoped location 105 guard let newUrl = fallback(url) else { return } 106 self.chooseMedia(processedEnum(newUrl)) 107 } 108 } 109 110 } 111 112 func makeCoordinator() -> Coordinator { 113 Coordinator(self) 114 } 115 116 func makeUIViewController(context: Context) -> PHPickerViewController { 117 var configuration = PHPickerConfiguration(photoLibrary: .shared()) 118 configuration.selectionLimit = 1 119 configuration.filter = imagesOnly ? .images : .any(of: [.images, .videos]) 120 121 let picker = PHPickerViewController(configuration: configuration) 122 picker.delegate = context.coordinator as any PHPickerViewControllerDelegate 123 return picker 124 } 125 126 func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) { 127 } 128 }