damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

MediaPicker.swift (7968B)


      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 enum MediaPickerEntry {
     13     case editPictureControl
     14     case postView
     15 }
     16 
     17 struct MediaPicker: UIViewControllerRepresentable {
     18 
     19     @Environment(\.presentationMode)
     20     @Binding private var presentationMode
     21     let mediaPickerEntry: MediaPickerEntry
     22 
     23     let onMediaSelected: (() -> Void)?
     24     let onMediaPicked: (PreUploadedMedia) -> Void
     25     
     26     init(mediaPickerEntry: MediaPickerEntry, onMediaSelected: (() -> Void)? = nil, onMediaPicked: @escaping (PreUploadedMedia) -> Void) {
     27         self.mediaPickerEntry = mediaPickerEntry
     28         self.onMediaSelected = onMediaSelected
     29         self.onMediaPicked = onMediaPicked
     30     }
     31 
     32     final class Coordinator: NSObject, PHPickerViewControllerDelegate {
     33         var parent: MediaPicker
     34         
     35         // properties used for returning medias in the same order as picking
     36         let dispatchGroup: DispatchGroup = DispatchGroup()
     37         var orderIds: [String] = []
     38         var orderMap: [String: PreUploadedMedia] = [:]
     39 
     40         init(_ parent: MediaPicker) {
     41             self.parent = parent
     42         }
     43         
     44         func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
     45             if results.isEmpty {
     46                 self.parent.presentationMode.dismiss()
     47             }
     48             
     49             // When user dismiss the upload confirmation and re-adds again, reset orderIds and orderMap
     50             orderIds.removeAll()
     51             orderMap.removeAll()
     52             
     53             for result in results {
     54                 
     55                 let orderId = result.assetIdentifier ?? UUID().uuidString
     56                 orderIds.append(orderId)
     57                 dispatchGroup.enter()
     58                 
     59                 if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
     60                     result.itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (item, error) in
     61                         guard let url = item as? URL else { return }
     62                         
     63                         if(url.pathExtension == "gif") {
     64                             // GIFs do not natively support location metadata (See https://superuser.com/a/556320 and https://www.w3.org/Graphics/GIF/spec-gif89a.txt)
     65                             // It is better to avoid any GPS data processing at all, as it can cause the image to be converted to JPEG.
     66                             // Therefore, we should load the file directtly and deliver it as "already processed".
     67                             
     68                             // Load the data for the GIF image
     69                             // - Don't load it as an UIImage since that can only get exported into JPEG/PNG
     70                             // - Don't load it as a file representation because it gets deleted before the upload can occur
     71                             _ = result.itemProvider.loadDataRepresentation(for: .gif, completionHandler: { imageData, error in
     72                                 guard let imageData else { return }
     73                                 let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: "gif")
     74                                 do {
     75                                     try imageData.write(to: destinationURL)
     76                                     Task {
     77                                         await self.chooseMedia(.processed_image(destinationURL), orderId: orderId)
     78                                     }
     79                                 }
     80                                 catch {
     81                                     Log.error("Failed to write GIF image data from Photo picker into a local copy", for: .image_uploading)
     82                                 }
     83                             })
     84                         }
     85                         else if canGetSourceTypeFromUrl(url: url) {
     86                             // Media was not taken from camera
     87                             self.attemptAcquireResourceAndChooseMedia(
     88                                 url: url,
     89                                 fallback: processImage,
     90                                 unprocessedEnum: {.unprocessed_image($0)},
     91                                 processedEnum: {.processed_image($0)},
     92                                 orderId: orderId)
     93                         } else {
     94                             // Media was taken from camera
     95                             result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
     96                                 if let image = image as? UIImage, error == nil {
     97                                     self.chooseMedia(.uiimage(image), orderId: orderId)
     98                                 }
     99                             }
    100                         }
    101                     }
    102                 } else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
    103                     result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { (url, error) in
    104                         guard let url, error == nil else { return }
    105 
    106                         self.attemptAcquireResourceAndChooseMedia(
    107                             url: url,
    108                             fallback: processVideo,
    109                             unprocessedEnum: {.unprocessed_video($0)},
    110                             processedEnum: {.processed_video($0)}, orderId: orderId
    111                         )
    112                     }
    113                 }
    114             }
    115             
    116             dispatchGroup.notify(queue: .main) { [weak self] in
    117                 guard let self = self else { return }
    118                 var arrMedia: [PreUploadedMedia] = []
    119                 for id in self.orderIds {
    120                     if let media = self.orderMap[id] {
    121                         arrMedia.append(media)
    122                         self.parent.onMediaPicked(media)
    123                     }
    124                 }
    125             }
    126         }
    127         
    128         
    129         private func chooseMedia(_ media: PreUploadedMedia, orderId: String) {
    130             self.parent.onMediaSelected?()
    131             self.orderMap[orderId] = media
    132             self.dispatchGroup.leave()
    133         }
    134         
    135         private func attemptAcquireResourceAndChooseMedia(url: URL, fallback: (URL) -> URL?, unprocessedEnum: (URL) -> PreUploadedMedia, processedEnum: (URL) -> PreUploadedMedia, orderId: String) {
    136             if url.startAccessingSecurityScopedResource() {
    137                 // Have permission from system to use url out of scope
    138                 print("Acquired permission to security scoped resource")
    139                 self.chooseMedia(unprocessedEnum(url), orderId: orderId)
    140             } else {
    141                 // Need to copy URL to non-security scoped location
    142                 guard let newUrl = fallback(url) else { return }
    143                 self.chooseMedia(processedEnum(newUrl), orderId: orderId)
    144             }
    145         }
    146 
    147     }
    148         
    149     func makeCoordinator() -> Coordinator {
    150         Coordinator(self)
    151     }
    152     
    153     func makeUIViewController(context: Context) -> PHPickerViewController {
    154         var configuration = PHPickerConfiguration(photoLibrary: .shared())
    155         switch mediaPickerEntry {
    156         case .postView:
    157             configuration.selectionLimit = 0 // allows multiple media selection
    158             configuration.filter = .any(of: [.images, .videos])
    159             configuration.selection = .ordered // images are returned in the order they were selected + numbered badge displayed
    160         case .editPictureControl:
    161             configuration.selectionLimit = 1 // allows one media selection
    162             configuration.filter = .images // allows image only
    163         }
    164         let picker = PHPickerViewController(configuration: configuration)
    165         picker.delegate = context.coordinator as any PHPickerViewControllerDelegate
    166         return picker
    167     }
    168 
    169     func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
    170     }
    171 }