176 lines
6.6 KiB
Swift
176 lines
6.6 KiB
Swift
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?()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} |