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?() } } } } }