You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 03:34:51