import SwiftUI import PhotosUI import os.log import AVKit /// 媒体类型 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] let imageSelectionLimit: Int let videoSelectionLimit: Int let onDismiss: (() -> Void)? class Coordinator: NSObject, PHPickerViewControllerDelegate { let parent: MediaPicker private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "MediaPicker") internal var currentPicker: PHPickerViewController? // 将 private 修改为 internal init(_ parent: MediaPicker) { self.parent = parent } func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { guard !results.isEmpty else { parent.onDismiss?() return } // 统计当前已选择的图片和视频数量 var currentImageCount = 0 var currentVideoCount = 0 for media in parent.selectedMedia { switch media { case .image: currentImageCount += 1 case .video: currentVideoCount += 1 } } // 检查新选择的项目 var newImages = 0 var newVideos = 0 for result in results { let itemProvider = result.itemProvider if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { newImages += 1 } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { newVideos += 1 } } // 检查是否超出限制 if (currentImageCount + newImages > parent.imageSelectionLimit) || (currentVideoCount + newVideos > parent.videoSelectionLimit) { // 准备错误信息 var message = "选择超出限制:\n" var limits: [String] = [] if currentImageCount + newImages > parent.imageSelectionLimit { limits.append("图片最多选择\(parent.imageSelectionLimit)张") } if currentVideoCount + newVideos > parent.videoSelectionLimit { limits.append("视频最多选择\(parent.videoSelectionLimit)个") } message += limits.joined(separator: "\n") // 显示提示 let alert = UIAlertController( title: "提示", message: message, preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "好的", style: .default) { _ in // 不清空选择,允许用户继续选择 }) // 显示提示框 picker.present(alert, animated: true) return } // 如果没有超出限制,处理选择的媒体 processSelectedMedia(results: results, picker: picker) } private func processSelectedMedia(results: [PHPickerResult], picker: PHPickerViewController) { var processedMedia = parent.selectedMedia let group = DispatchGroup() for result in results { let itemProvider = result.itemProvider if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { group.enter() processImage(itemProvider: itemProvider) { media in if let media = media { DispatchQueue.main.async { processedMedia.append(media) } } group.leave() } } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { group.enter() processVideo(itemProvider: itemProvider) { media in if let media = media { DispatchQueue.main.async { processedMedia.append(media) } } group.leave() } } } group.notify(queue: .main) { self.parent.selectedMedia = processedMedia picker.dismiss(animated: true) { self.parent.onDismiss?() } } } 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 } 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) } } } } func makeUIViewController(context: Context) -> PHPickerViewController { var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) configuration.filter = .any(of: [.videos, .images]) configuration.selectionLimit = 0 // 设置为0表示不限制选择数量,我们在代码中处理 configuration.preferredAssetRepresentationMode = .current let picker = PHPickerViewController(configuration: configuration) picker.delegate = context.coordinator context.coordinator.currentPicker = picker return picker } func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) { // 更新视图控制器(如果需要) } func makeCoordinator() -> Coordinator { Coordinator(self) } }