SwiftUI中PhotosPicker的loadTransferable无法响应取消,如何实现加载超时?
问题分析与解决方案
核心问题
你的代码逻辑本身没问题,但loadTransferable(type:)底层的照片加载操作不响应任务取消:当超时任务触发group.cancelAll()后,图片加载任务仍会继续执行,而withTaskGroup必须等待所有添加的任务完成才会退出,导致整个函数卡在withTaskGroup块中,后续的switch代码永远无法执行。
可行解决方案
方案1:使用官方Task.withTimeout(iOS 16+/macOS 13+)
苹果在iOS 16后提供了原生超时API,代码最简洁,且能保证超时后立即返回结果(即使底层加载任务未终止,也不会阻塞当前函数):
func loadImage(_ image: PhotosPickerItem, timeout milliseconds: Int) async throws -> UIImage { try await Task.withTimeout(nanoseconds: UInt64(milliseconds) * 1_000_000) { guard let imageData = try await image.loadTransferable(type: Data.self), let uiImage = UIImage(data: imageData) else { throw MyError.imageCouldNotBeLoaded(details: "Image transfer or decoding issues.") } return uiImage } onTimeout: { throw MyError.imageCouldNotBeLoaded(details: "Timeout occurred.") } }
方案2:async let + 并行任务(iOS 15+兼容)
通过async let启动并行任务,利用任务优先级实现超时逻辑,同样能保证函数及时返回:
func loadImage(_ image: PhotosPickerItem, timeout milliseconds: Int) async throws -> UIImage { // 图片加载任务 async let loadTask: UIImage = { guard let imageData = try await image.loadTransferable(type: Data.self), let uiImage = UIImage(data: imageData) else { throw MyError.imageCouldNotBeLoaded(details: "Image transfer or decoding issues.") } return uiImage }() // 超时任务 async let timeoutTask: Error = { try await Task.sleep(nanoseconds: UInt64(milliseconds) * 1_000_000) throw MyError.imageCouldNotBeLoaded(details: "Timeout occurred.") }() do { // 优先等待加载任务完成,成功则取消超时任务 let image = try await loadTask timeoutTask.cancel() return image } catch { // 加载失败,取消超时任务并抛出错误 timeoutTask.cancel() throw error } }
方案3:使用PHAsset主动控制加载(真正终止任务)
如果需要彻底终止底层加载操作(避免后台资源浪费),可以直接使用Photos框架的PHImageManager,通过请求ID主动取消加载:
import Photos func loadImage(_ image: PhotosPickerItem, timeout milliseconds: Int) async throws -> UIImage { // 从PhotosPickerItem获取PHAsset guard let assetID = image.itemIdentifier, let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetID], options: nil).firstObject else { throw MyError.imageCouldNotBeLoaded(details: "Failed to get PHAsset.") } let semaphore = DispatchSemaphore(value: 0) var result: Result<UIImage, Error>! var requestID: PHImageRequestID! let options = PHImageRequestOptions() options.deliveryMode = .highQualityFormat options.isNetworkAccessAllowed = true // 允许网络加载高清图 // 发起图片加载请求 requestID = PHImageManager.default().requestImage( for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: options ) { image, info in defer { semaphore.signal() } if let error = info?[PHImageErrorKey] as? Error { result = .failure(error) return } result = image.map(.success) ?? .failure(MyError.imageCouldNotBeLoaded(details: "Failed to load image.")) } // 启动超时计时器,到点取消加载请求 DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(milliseconds)) { PHImageManager.default().cancelImageRequest(requestID) result = .failure(MyError.imageCouldNotBeLoaded(details: "Timeout occurred.")) semaphore.signal() } semaphore.wait() switch result { case .success(let image): return image case .failure(let error): throw error case .none: throw MyError.imageCouldNotBeLoaded(details: "Unknown error.") } }
注意:此方案需要在Info.plist中添加
NSPhotoLibraryUsageDescription权限描述。
内容的提问来源于stack exchange,提问作者Sebastian
相关产品推荐
相关产品推荐

