wake-ios/wake/View/Components/Upload/MediaPicker.swift
2025-08-22 18:58:08 +08:00

304 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
///
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
}
}
}
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
///
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) {
self._selectedMedia = selectedMedia
self.imageSelectionLimit = imageSelectionLimit
self.videoSelectionLimit = videoSelectionLimit
self.allowedMediaTypes = allowedMediaTypes
self.selectionMode = selectionMode
self.onDismiss = onDismiss
}
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
}
//
processSelectedMedia(results: results, picker: picker, processedMedia: &processedMedia)
}
private func processSelectedMedia(results: [PHPickerResult],
picker: PHPickerViewController,
processedMedia: inout [MediaType]) {
let group = DispatchGroup()
let mediaCollector = MediaCollector()
for result in results {
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)
}
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)
}
group.leave()
}
}
}
// Create a local copy of the parent reference
let parent = self.parent
group.notify(queue: .main) {
let finalMedia = mediaCollector.mediaItems
parent.selectedMedia = finalMedia
picker.dismiss(animated: true) {
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)
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)
}
}
}