wake-ios/wake/View/Components/Upload/MediaPicker.swift
2025-08-21 11:08:21 +08:00

224 lines
8.7 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
}
}
}
struct MediaPicker: UIViewControllerRepresentable {
@Binding var selectedMedia: [MediaType]
let imageSelectionLimit: Int
let videoSelectionLimit: Int
let onDismiss: (() -> Void)?
class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: MediaPicker
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "MediaPicker")
internal var currentPicker: PHPickerViewController? // private internal
init(_ parent: MediaPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
guard !results.isEmpty else {
parent.onDismiss?()
return
}
//
var currentImageCount = 0
var currentVideoCount = 0
for media in parent.selectedMedia {
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) {
newImages += 1
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
newVideos += 1
}
}
//
if (currentImageCount + newImages > parent.imageSelectionLimit) ||
(currentVideoCount + newVideos > parent.videoSelectionLimit) {
//
var message = "选择超出限制:\n"
var limits: [String] = []
if currentImageCount + newImages > parent.imageSelectionLimit {
limits.append("图片最多选择\(parent.imageSelectionLimit)")
}
if currentVideoCount + newVideos > parent.videoSelectionLimit {
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)
}
private func processSelectedMedia(results: [PHPickerResult], picker: PHPickerViewController) {
var processedMedia = parent.selectedMedia
let group = DispatchGroup()
for result in results {
let itemProvider = result.itemProvider
if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
group.enter()
processImage(itemProvider: itemProvider) { media in
if let media = media {
DispatchQueue.main.async {
processedMedia.append(media)
}
}
group.leave()
}
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
group.enter()
processVideo(itemProvider: itemProvider) { media in
if let media = media {
DispatchQueue.main.async {
processedMedia.append(media)
}
}
group.leave()
}
}
}
group.notify(queue: .main) {
self.parent.selectedMedia = processedMedia
picker.dismiss(animated: true) {
self.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)
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)
}
}
}
}
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.filter = .any(of: [.videos, .images])
configuration.selectionLimit = 0 // 0
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)
}
}