wake-ios/wake/View/Components/Upload/MediaPicker.swift
2025-08-21 19:39:53 +08:00

176 lines
6.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import PhotosUI
import AVFoundation
import os.log
///
public enum MediaType: Equatable {
case image(UIImage)
case video(URL, UIImage?) // URL UIImage
public var thumbnail: UIImage? {
switch self {
case .image(let image):
return image
case .video(_, let thumbnail):
return thumbnail
}
}
public var isVideo: Bool {
if case .video = self {
return true
}
return false
}
public static func == (lhs: MediaType, rhs: MediaType) -> Bool {
switch (lhs, rhs) {
case (.image(let lhsImage), .image(let rhsImage)):
return lhsImage.pngData() == rhsImage.pngData()
case (.video(let lhsURL, _), .video(let rhsURL, _)):
return lhsURL == rhsURL
default:
return false
}
}
}
struct MediaPicker: UIViewControllerRepresentable {
@Binding var selectedMedia: [MediaType]
var selectionLimit: Int
var onDismiss: (() -> Void)?
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
//
configuration.filter = .any(of: [.videos, .images])
configuration.selectionLimit = selectionLimit
configuration.preferredAssetRepresentationMode = .current
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: MediaPicker
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "MediaPicker")
private var processedCount = 0
private var totalToProcess = 0
private var tempMedia: [MediaType] = []
init(_ parent: MediaPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
guard !results.isEmpty else {
parent.onDismiss?()
return
}
processedCount = 0
totalToProcess = results.count
tempMedia = []
for result in results {
let itemProvider = result.itemProvider
if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
processImage(itemProvider: itemProvider) { [weak self] media in
self?.handleProcessedMedia(media)
}
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
processVideo(itemProvider: itemProvider) { [weak self] media in
self?.handleProcessedMedia(media)
}
} else {
processedCount += 1
checkCompletion()
}
}
}
private func processImage(itemProvider: NSItemProvider, completion: @escaping (MediaType?) -> Void) {
itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
if let image = object as? UIImage {
completion(.image(image))
} else {
self.logger.error("Failed to load image: \(error?.localizedDescription ?? "Unknown error")")
completion(nil)
}
}
}
private func processVideo(itemProvider: NSItemProvider, completion: @escaping (MediaType?) -> Void) {
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in
guard let videoURL = url, error == nil else {
self.logger.error("Failed to load video: \(error?.localizedDescription ?? "Unknown error")")
completion(nil)
return
}
// URL
let tempDirectory = FileManager.default.temporaryDirectory
let targetURL = tempDirectory.appendingPathComponent("\(UUID().uuidString).\(videoURL.pathExtension)")
do {
//
if FileManager.default.fileExists(atPath: targetURL.path) {
try FileManager.default.removeItem(at: targetURL)
}
try FileManager.default.copyItem(at: videoURL, to: targetURL)
//
MediaUtils.extractFirstFrame(from: targetURL) { result in
switch result {
case .success(let thumbnail):
completion(.video(targetURL, thumbnail))
case .failure(let error):
self.logger.error("Failed to extract video thumbnail: \(error.localizedDescription)")
// 使
completion(.video(targetURL, nil))
}
}
} catch {
self.logger.error("Failed to copy video file: \(error.localizedDescription)")
completion(nil)
}
}
}
private func handleProcessedMedia(_ media: MediaType?) {
if let media = media {
DispatchQueue.main.async { [weak self] in
self?.tempMedia.append(media)
self?.checkCompletion()
}
} else {
processedCount += 1
checkCompletion()
}
}
private func checkCompletion() {
processedCount += 1
if processedCount >= totalToProcess {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if !self.tempMedia.isEmpty {
self.parent.selectedMedia = self.tempMedia
}
self.parent.onDismiss?()
}
}
}
}
}