Files
damus/damus/Views/MediaPicker.swift
Swift Coder e599ef1ac9 Fix duplicate uploads
Reset orderIds and orderMap

Changelog-Fixed: Fix duplicate uploads
Signed-off-by: Swift Coder <scoder1747@gmail.com>
2024-12-03 17:31:55 +09:00

166 lines
7.5 KiB
Swift

//
// ImagePicker.swift
// damus
//
// Created by Swift on 3/31/23.
//
import UIKit
import SwiftUI
import PhotosUI
enum MediaPickerEntry {
case editPictureControl
case postView
}
struct MediaPicker: UIViewControllerRepresentable {
@Environment(\.presentationMode)
@Binding private var presentationMode
let mediaPickerEntry: MediaPickerEntry
@Binding var image_upload_confirm: Bool
let onMediaPicked: (PreUploadedMedia) -> Void
final class Coordinator: NSObject, PHPickerViewControllerDelegate {
var parent: MediaPicker
// properties used for returning medias in the same order as picking
let dispatchGroup: DispatchGroup = DispatchGroup()
var orderIds: [String] = []
var orderMap: [String: PreUploadedMedia] = [:]
init(_ parent: MediaPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
if results.isEmpty {
self.parent.presentationMode.dismiss()
}
// When user dismiss the upload confirmation and re-adds again, reset orderIds and orderMap
orderIds.removeAll()
orderMap.removeAll()
for result in results {
let orderId = result.assetIdentifier ?? UUID().uuidString
orderIds.append(orderId)
dispatchGroup.enter()
if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
result.itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (item, error) in
guard let url = item as? URL else { return }
if(url.pathExtension == "gif") {
// GIFs do not natively support location metadata (See https://superuser.com/a/556320 and https://www.w3.org/Graphics/GIF/spec-gif89a.txt)
// It is better to avoid any GPS data processing at all, as it can cause the image to be converted to JPEG.
// Therefore, we should load the file directtly and deliver it as "already processed".
// Load the data for the GIF image
// - Don't load it as an UIImage since that can only get exported into JPEG/PNG
// - Don't load it as a file representation because it gets deleted before the upload can occur
_ = result.itemProvider.loadDataRepresentation(for: .gif, completionHandler: { imageData, error in
guard let imageData else { return }
let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: "gif")
do {
try imageData.write(to: destinationURL)
Task {
await self.chooseMedia(.processed_image(destinationURL), orderId: orderId)
}
}
catch {
Log.error("Failed to write GIF image data from Photo picker into a local copy", for: .image_uploading)
}
})
}
else if canGetSourceTypeFromUrl(url: url) {
// Media was not taken from camera
self.attemptAcquireResourceAndChooseMedia(
url: url,
fallback: processImage,
unprocessedEnum: {.unprocessed_image($0)},
processedEnum: {.processed_image($0)},
orderId: orderId)
} else {
// Media was taken from camera
result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
if let image = image as? UIImage, error == nil {
self.chooseMedia(.uiimage(image), orderId: orderId)
}
}
}
}
} else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { (url, error) in
guard let url, error == nil else { return }
self.attemptAcquireResourceAndChooseMedia(
url: url,
fallback: processVideo,
unprocessedEnum: {.unprocessed_video($0)},
processedEnum: {.processed_video($0)}, orderId: orderId
)
}
}
}
dispatchGroup.notify(queue: .main) { [weak self] in
guard let self = self else { return }
var arrMedia: [PreUploadedMedia] = []
for id in self.orderIds {
if let media = self.orderMap[id] {
arrMedia.append(media)
self.parent.onMediaPicked(media)
}
}
}
}
private func chooseMedia(_ media: PreUploadedMedia, orderId: String) {
self.parent.image_upload_confirm = true
self.orderMap[orderId] = media
self.dispatchGroup.leave()
}
private func attemptAcquireResourceAndChooseMedia(url: URL, fallback: (URL) -> URL?, unprocessedEnum: (URL) -> PreUploadedMedia, processedEnum: (URL) -> PreUploadedMedia, orderId: String) {
if url.startAccessingSecurityScopedResource() {
// Have permission from system to use url out of scope
print("Acquired permission to security scoped resource")
self.chooseMedia(unprocessedEnum(url), orderId: orderId)
} else {
// Need to copy URL to non-security scoped location
guard let newUrl = fallback(url) else { return }
self.chooseMedia(processedEnum(newUrl), orderId: orderId)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration(photoLibrary: .shared())
switch mediaPickerEntry {
case .postView:
configuration.selectionLimit = 0 // allows multiple media selection
configuration.filter = .any(of: [.images, .videos])
configuration.selection = .ordered // images are returned in the order they were selected + numbered badge displayed
case .editPictureControl:
configuration.selectionLimit = 1 // allows one media selection
configuration.filter = .images // allows image only
}
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator as any PHPickerViewControllerDelegate
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
}
}