wake-ios/wake/View/Components/Upload/MediaPicker.swift
2025-09-01 19:42:32 +08:00

286 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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