feat: 媒体上传
This commit is contained in:
parent
1fca9f413c
commit
1e6305ec35
84
wake/Utils/MediaUtils.swift
Normal file
84
wake/Utils/MediaUtils.swift
Normal file
@ -0,0 +1,84 @@
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import os.log
|
||||
|
||||
/// 媒体工具类,提供视频处理相关功能
|
||||
enum MediaUtils {
|
||||
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "MediaUtils")
|
||||
|
||||
/// 从视频URL中提取第一帧
|
||||
/// - Parameters:
|
||||
/// - videoURL: 视频文件的URL
|
||||
/// - completion: 完成回调,返回UIImage或错误
|
||||
static func extractFirstFrame(from videoURL: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
|
||||
let asset = AVURLAsset(url: videoURL)
|
||||
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
|
||||
assetImgGenerate.appliesPreferredTrackTransform = true
|
||||
|
||||
// 获取视频时长
|
||||
let duration = asset.duration
|
||||
let durationTime = CMTimeGetSeconds(duration)
|
||||
|
||||
// 如果视频时长小于等于0,返回错误
|
||||
guard durationTime > 0 else {
|
||||
let error = NSError(domain: "com.yourapp.media", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid video duration"])
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取第一帧(时间点为0)
|
||||
let time = CMTime(seconds: 0, preferredTimescale: 600)
|
||||
|
||||
// 生成图片
|
||||
assetImgGenerate.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { (_, cgImage, _, result, error) in
|
||||
if let error = error {
|
||||
logger.error("Failed to generate image: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard result == .succeeded, let cgImage = cgImage else {
|
||||
let error = NSError(domain: "com.yourapp.media", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to generate image from video"])
|
||||
logger.error("Failed to generate image: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 创建UIImage并返回
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(image))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 从视频数据中提取第一帧
|
||||
/// - Parameters:
|
||||
/// - videoData: 视频数据
|
||||
/// - completion: 完成回调,返回UIImage或错误
|
||||
static func extractFirstFrame(from videoData: Data, completion: @escaping (Result<UIImage, Error>) -> Void) {
|
||||
// 创建临时文件URL
|
||||
let tempDirectoryURL = FileManager.default.temporaryDirectory
|
||||
let fileName = "tempVideo_\(UUID().uuidString).mov"
|
||||
let fileURL = tempDirectoryURL.appendingPathComponent(fileName)
|
||||
|
||||
do {
|
||||
// 将数据写入临时文件
|
||||
try videoData.write(to: fileURL)
|
||||
|
||||
// 调用URL版本的方法
|
||||
extractFirstFrame(from: fileURL) { result in
|
||||
// 清理临时文件
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
completion(result)
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to write video data to temporary file: \(error.localizedDescription)")
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
221
wake/View/Components/Upload/MediaPicker.swift
Normal file
221
wake/View/Components/Upload/MediaPicker.swift
Normal file
@ -0,0 +1,221 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import AVFoundation
|
||||
import os.log
|
||||
|
||||
/// 媒体类型
|
||||
enum MediaType: Equatable {
|
||||
case image(UIImage)
|
||||
case video(URL, UIImage?) // URL 是视频地址,UIImage 是视频缩略图
|
||||
|
||||
var thumbnail: UIImage? {
|
||||
switch self {
|
||||
case .image(let image):
|
||||
return image
|
||||
case .video(_, let thumbnail):
|
||||
return thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
var isVideo: Bool {
|
||||
if case .video = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 预览辅助视图
|
||||
struct MediaThumbnailView: View {
|
||||
let media: MediaType
|
||||
let onDelete: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
// 显示缩略图
|
||||
if let thumbnail = media.thumbnail {
|
||||
Image(uiImage: thumbnail)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 80, height: 80)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
} else {
|
||||
Color.gray
|
||||
.frame(width: 80, height: 80)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
// 视频标识
|
||||
if media.isVideo {
|
||||
Image(systemName: "video.fill")
|
||||
.foregroundColor(.white)
|
||||
.padding(4)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.clipShape(Circle())
|
||||
.padding(4)
|
||||
}
|
||||
|
||||
// 删除按钮
|
||||
if let onDelete = onDelete {
|
||||
Button(action: onDelete) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.red)
|
||||
.background(Color.white)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.offset(x: 8, y: -8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
wake/View/Components/Upload/MediaUploader.swift
Normal file
138
wake/View/Components/Upload/MediaUploader.swift
Normal file
@ -0,0 +1,138 @@
|
||||
import SwiftUI
|
||||
import os.log
|
||||
|
||||
class MediaUploader: ObservableObject {
|
||||
@Published var selectedMedia: [MediaType] = []
|
||||
@Published private(set) var isUploading = false
|
||||
@Published private(set) var uploadProgress: [Int: Double] = [:] // 跟踪每个上传任务的进度
|
||||
@Published var showError = false
|
||||
@Published private(set) var errorMessage = ""
|
||||
|
||||
@Published var showMediaPicker = false
|
||||
let maxSelection: Int
|
||||
var onUploadComplete: ([(MediaType, URL)]?) -> Void
|
||||
var uploadFunction: (MediaType, @escaping (Double) -> Void) async throws -> URL
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "MediaUploader")
|
||||
|
||||
init(maxSelection: Int,
|
||||
onUploadComplete: @escaping ([(MediaType, URL)]?) -> Void,
|
||||
uploadFunction: @escaping (MediaType, @escaping (Double) -> Void) async throws -> URL) {
|
||||
self.maxSelection = maxSelection
|
||||
self.onUploadComplete = onUploadComplete
|
||||
self.uploadFunction = uploadFunction
|
||||
}
|
||||
|
||||
// 显示媒体选择器
|
||||
func showPicker() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.showMediaPicker = true
|
||||
}
|
||||
}
|
||||
|
||||
// 公开的方法供外部调用
|
||||
func startUpload() async -> [(MediaType, URL)]? {
|
||||
guard !selectedMedia.isEmpty else { return nil }
|
||||
|
||||
isUploading = true
|
||||
uploadProgress.removeAll()
|
||||
|
||||
do {
|
||||
var uploadedResults: [(MediaType, URL)] = []
|
||||
|
||||
for (index, media) in selectedMedia.enumerated() {
|
||||
let url = try await uploadFunction(media) { [weak self] progress in
|
||||
DispatchQueue.main.async {
|
||||
self?.uploadProgress[index] = progress
|
||||
}
|
||||
}
|
||||
uploadedResults.append((media, url))
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.isUploading = false
|
||||
self.onUploadComplete(uploadedResults)
|
||||
self.selectedMedia.removeAll()
|
||||
}
|
||||
|
||||
return uploadedResults
|
||||
} catch {
|
||||
logger.error("上传失败: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.errorMessage = "上传失败: \(error.localizedDescription)"
|
||||
self.showError = true
|
||||
self.isUploading = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MediaUploaderView: View {
|
||||
@ObservedObject var mediaUploader: MediaUploader
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// 显示已选媒体数量
|
||||
if !mediaUploader.selectedMedia.isEmpty {
|
||||
Text("已选择 \(mediaUploader.selectedMedia.count) 个文件")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 2)
|
||||
.fullScreenCover(isPresented: $mediaUploader.showMediaPicker) {
|
||||
MediaPicker(
|
||||
selectedMedia: $mediaUploader.selectedMedia,
|
||||
selectionLimit: mediaUploader.maxSelection
|
||||
) {
|
||||
mediaUploader.showMediaPicker = false
|
||||
}
|
||||
}
|
||||
.alert("错误", isPresented: $mediaUploader.showError) {
|
||||
Button("确定", role: .cancel) {}
|
||||
} message: {
|
||||
Text(mediaUploader.errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 预览
|
||||
struct MediaUploader_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
MediaUploaderView(
|
||||
mediaUploader: MediaUploader(
|
||||
maxSelection: 5,
|
||||
onUploadComplete: { _ in },
|
||||
uploadFunction: { _, _ in
|
||||
// 模拟上传
|
||||
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
return URL(string: "https://example.com/uploaded")!
|
||||
}
|
||||
)
|
||||
)
|
||||
.padding()
|
||||
.previewLayout(.sizeThatFits)
|
||||
|
||||
MediaUploaderView(
|
||||
mediaUploader: MediaUploader(
|
||||
maxSelection: 5,
|
||||
onUploadComplete: { _ in },
|
||||
uploadFunction: { _, _ in
|
||||
// 模拟上传
|
||||
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
return URL(string: "https://example.com/uploaded")!
|
||||
}
|
||||
)
|
||||
)
|
||||
.padding()
|
||||
.previewLayout(.sizeThatFits)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
88
wake/View/Components/Upload/VideoPicker.swift
Normal file
88
wake/View/Components/Upload/VideoPicker.swift
Normal file
@ -0,0 +1,88 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import AVFoundation
|
||||
import os.log
|
||||
|
||||
struct VideoPicker: UIViewControllerRepresentable {
|
||||
@Binding var selectedVideoURL: URL?
|
||||
@Binding var thumbnailImage: UIImage?
|
||||
var onDismiss: (() -> Void)?
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||
configuration.filter = .videos
|
||||
configuration.selectionLimit = 1
|
||||
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: VideoPicker
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "VideoPicker")
|
||||
|
||||
init(_ parent: VideoPicker) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
guard let result = results.first else {
|
||||
parent.onDismiss?()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取视频URL
|
||||
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { [weak self] url, error in
|
||||
guard let self = self, let videoURL = url, error == nil else {
|
||||
self?.logger.error("Failed to load video: \(error?.localizedDescription ?? "Unknown error")")
|
||||
DispatchQueue.main.async {
|
||||
self?.parent.onDismiss?()
|
||||
}
|
||||
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) { [weak self] result in
|
||||
switch result {
|
||||
case .success(let image):
|
||||
DispatchQueue.main.async {
|
||||
self?.parent.thumbnailImage = image
|
||||
self?.parent.selectedVideoURL = targetURL
|
||||
self?.parent.onDismiss?()
|
||||
}
|
||||
case .failure(let error):
|
||||
self?.logger.error("Failed to extract video thumbnail: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
self?.parent.onDismiss?()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
self.logger.error("Failed to copy video file: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
self.parent.onDismiss?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
wake/View/Examples/MediaUpload.swift
Normal file
104
wake/View/Examples/MediaUpload.swift
Normal file
@ -0,0 +1,104 @@
|
||||
import SwiftUI
|
||||
import os.log
|
||||
|
||||
public struct ExampleView: View {
|
||||
@StateObject private var mediaUploader = MediaUploader(
|
||||
maxSelection: 5,
|
||||
onUploadComplete: { _ in },
|
||||
uploadFunction: { media, progress in
|
||||
// 模拟上传进度
|
||||
for i in 0...10 {
|
||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒
|
||||
progress(Double(i) / 10.0)
|
||||
}
|
||||
|
||||
// 这里替换为您的实际上传逻辑
|
||||
switch media {
|
||||
case .image(let image):
|
||||
// 上传图片
|
||||
guard let url = URL(string: "https://example.com/images/\(UUID().uuidString).jpg") else {
|
||||
throw NSError(domain: "com.example.app", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])
|
||||
}
|
||||
return url
|
||||
case .video(let url, _):
|
||||
// 上传视频
|
||||
guard let url = URL(string: "https://example.com/videos/\(UUID().uuidString).mp4") else {
|
||||
throw NSError(domain: "com.example.app", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])
|
||||
}
|
||||
return url
|
||||
}
|
||||
}
|
||||
)
|
||||
@State private var uploadedURLs: [URL] = []
|
||||
@EnvironmentObject private var authState: AuthState
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
NavigationView {
|
||||
VStack {
|
||||
// 添加媒体按钮
|
||||
Button(action: {
|
||||
mediaUploader.showPicker()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
Text("添加媒体")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
// 媒体上传组件
|
||||
MediaUploaderView(mediaUploader: mediaUploader)
|
||||
.onChange(of: mediaUploader.selectedMedia) { newValue in
|
||||
if !newValue.isEmpty {
|
||||
Task {
|
||||
if let results = await mediaUploader.startUpload() {
|
||||
let urls = results.map { $0.1 }
|
||||
uploadedURLs.append(contentsOf: urls)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显示已上传的URL
|
||||
List(uploadedURLs, id: \.self) { url in
|
||||
Text(url.absoluteString)
|
||||
.font(.caption)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
}
|
||||
.navigationTitle("媒体上传示例")
|
||||
}
|
||||
}
|
||||
|
||||
// 实际上传函数示例
|
||||
private func uploadMedia(_ media: MediaType, progress: @escaping (Double) -> Void) async throws -> URL {
|
||||
// 模拟上传进度
|
||||
for i in 0...10 {
|
||||
try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒
|
||||
progress(Double(i) / 10.0)
|
||||
}
|
||||
|
||||
// 这里替换为您的实际上传逻辑
|
||||
switch media {
|
||||
case .image(let image):
|
||||
// 上传图片
|
||||
return URL(string: "https://example.com/images/\(UUID().uuidString).jpg")!
|
||||
case .video(let url, _):
|
||||
// 上传视频
|
||||
return URL(string: "https://example.com/videos/\(UUID().uuidString).mp4")!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ExampleView()
|
||||
.environmentObject(AuthState.shared)
|
||||
}
|
||||
@ -46,7 +46,7 @@ struct WakeApp: App {
|
||||
// 已登录:显示userInfo页面
|
||||
// UserInfo()
|
||||
// .environmentObject(authState)
|
||||
MultiImageUploadExampleView()
|
||||
ExampleView()
|
||||
.environmentObject(authState)
|
||||
} else {
|
||||
// 未登录:显示登录界面
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user