286 lines
11 KiB
Swift
286 lines
11 KiB
Swift
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)
|
||
}
|
||
}
|
||
}
|