import SwiftUI import PhotosUI import os.log import AVKit enum MediaTypeFilter { case imagesOnly case videosOnly case all var pickerFilter: PHPickerFilter { switch self { case .imagesOnly: return .images case .videosOnly: return .videos case .all: return .any(of: [.videos, .images]) } } } struct MediaPicker: UIViewControllerRepresentable { @Binding var selectedMedia: [MediaType] let imageSelectionLimit: Int let videoSelectionLimit: Int let onDismiss: (() -> Void)? let allowedMediaTypes: MediaTypeFilter let selectionMode: SelectionMode let onUploadProgress: ((Int, Double) -> Void)? /// 选择模式 enum SelectionMode { case single // 单选模式 case multiple // 多选模式 var selectionLimit: Int { switch self { case .single: return 1 case .multiple: return 0 // 0 表示不限制选择数量,由 imageSelectionLimit 和 videoSelectionLimit 控制 } } } init(selectedMedia: Binding<[MediaType]>, imageSelectionLimit: Int = 10, videoSelectionLimit: Int = 10, allowedMediaTypes: MediaTypeFilter = .all, selectionMode: SelectionMode = .multiple, onDismiss: (() -> Void)? = nil, onUploadProgress: ((Int, Double) -> Void)? = nil) { self._selectedMedia = selectedMedia self.imageSelectionLimit = imageSelectionLimit self.videoSelectionLimit = videoSelectionLimit self.allowedMediaTypes = allowedMediaTypes self.selectionMode = selectionMode self.onDismiss = onDismiss self.onUploadProgress = onUploadProgress } func makeUIViewController(context: Context) -> PHPickerViewController { var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) configuration.filter = allowedMediaTypes.pickerFilter configuration.selectionLimit = selectionMode.selectionLimit 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) } class Coordinator: NSObject, PHPickerViewControllerDelegate { let parent: MediaPicker private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "MediaPicker") internal var currentPicker: PHPickerViewController? init(_ parent: MediaPicker) { self.parent = parent } func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { guard !results.isEmpty else { parent.onDismiss?() return } // 如果是单选模式,清空之前的选择 var processedMedia = parent.selectionMode == .single ? [] : parent.selectedMedia // 统计当前已选择的图片和视频数量 var currentImageCount = 0 var currentVideoCount = 0 for media in processedMedia { 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) { guard parent.allowedMediaTypes != .videosOnly else { continue } newImages += 1 } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { guard parent.allowedMediaTypes != .imagesOnly else { continue } newVideos += 1 } } // 检查是否超出限制 if (currentImageCount + newImages > parent.imageSelectionLimit) || (currentVideoCount + newVideos > parent.videoSelectionLimit) { // 准备错误信息 var message = "选择超出限制:\n" var limits: [String] = [] if currentImageCount + newImages > parent.imageSelectionLimit && parent.allowedMediaTypes != .videosOnly { limits.append("图片最多选择\(parent.imageSelectionLimit)张") } if currentVideoCount + newVideos > parent.videoSelectionLimit && parent.allowedMediaTypes != .imagesOnly { 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 } // 先关闭选择器 picker.dismiss(animated: true) { [weak self] in guard let self = self else { return } // 处理选中的媒体 var processedMedia = self.parent.selectionMode == .single ? [] : self.parent.selectedMedia self.processSelectedMedia(results: results, picker: picker, processedMedia: &processedMedia) // 调用 onDismiss 通知外部选择器已关闭 self.parent.onDismiss?() } } private func processSelectedMedia(results: [PHPickerResult], picker: PHPickerViewController, processedMedia: inout [MediaType]) { let group = DispatchGroup() let mediaCollector = MediaCollector() for (index, result) in results.enumerated() { let itemProvider = result.itemProvider if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { guard parent.allowedMediaTypes != .videosOnly else { continue } group.enter() processImage(itemProvider: itemProvider) { media in if let media = media { mediaCollector.add(media: media) // 更新上传进度 DispatchQueue.main.async { self.parent.onUploadProgress?(index, 1.0) // 图片直接完成 } } group.leave() } } else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { guard parent.allowedMediaTypes != .imagesOnly else { continue } group.enter() processVideo(itemProvider: itemProvider) { media in if let media = media { mediaCollector.add(media: media) // 更新上传进度 DispatchQueue.main.async { self.parent.onUploadProgress?(index, 1.0) // 视频直接完成 } } group.leave() } } } group.notify(queue: .main) { let finalMedia = mediaCollector.mediaItems self.parent.selectedMedia = finalMedia } } 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) if let thumbnail = self.generateThumbnail(for: targetURL) { completion(.video(targetURL, thumbnail)) } else { completion(.video(targetURL, nil)) } } catch { self.logger.error("Failed to copy video file: \(error.localizedDescription)") completion(nil) } } } private func generateThumbnail(for videoURL: URL) -> UIImage? { let asset = AVAsset(url: videoURL) let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true do { let cgImage = try imageGenerator.copyCGImage(at: CMTime(seconds: 0, preferredTimescale: 1), actualTime: nil) return UIImage(cgImage: cgImage) } catch { logger.error("Failed to generate thumbnail: \(error.localizedDescription)") return nil } } } } // Helper class to collect media items in a thread-safe way private class MediaCollector { private let queue = DispatchQueue(label: "com.example.MediaCollector", attributes: .concurrent) private var _mediaItems: [MediaType] = [] var mediaItems: [MediaType] { queue.sync { _mediaItems } } func add(media: MediaType) { queue.async(flags: .barrier) { [weak self] in self?._mediaItems.append(media) } } }