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

SwiftUI中URLSession限制请求数遇异步转换错误求助

异步图片加载器封装Operation时的异步兼容问题

我正在实现基于URLRequest的异步图片加载器,想把下载逻辑封装进自定义Operation中,用OperationQueue的maxConcurrentOperationCount限制同时下载数为3。已经重写了Operation类支持异步下载,但遇到错误:

Invalid conversion from 'async' function of type '(URL?, URLResponse?, (any Error)?) async throws -> Void' to synchronous function type '(URL?, URLResponse?, (any Error)?) -> Void'

推测问题出在下载函数放在Task组内。以下是相关代码:

自定义DownloadOperation类

class DownloadOperation: Operation {
    private var task: URLSessionDataTask!
    
    init(session: URLSession, downloadTaskURL: URLRequest, completionHandler: ((URL?, URLResponse?, Error?) -> Void)?) {
           super.init()
           
           // use weak self to prevent retain cycle
           task = session.dataTask(with: downloadTaskURL, completionHandler: { [weak self] (URLRequest, response, error) in
               
              /*
                set the operation state to finished once
                the download task is completed or have error
              */
               self?.state = .finished
           })
       }
    
    enum OperationState : Int {
            case ready
            case executing
            case finished
        }

    private var state : OperationState = .ready {
          willSet {
              self.willChangeValue(forKey: "isExecuting")
              self.willChangeValue(forKey: "isFinished")
          }
          
          didSet {
              self.didChangeValue(forKey: "isExecuting")
              self.didChangeValue(forKey: "isFinished")
          }
      }
      
      override var isReady: Bool { return state == .ready }
      override var isExecuting: Bool { return state == .executing }
      override var isFinished: Bool { return state == .finished }
    
    
    override func start() {
         /*
         if the operation or queue got cancelled even
         before the operation has started, set the
         operation state to finished and return
         */
         if(self.isCancelled) {
             state = .finished
             return
         }
         
         // set the state to executing
         state = .executing
         
         print("downloading")
               
         // start the downloading
         self.task.resume()
     }

     override func cancel() {
         super.cancel()
       
         // cancel the downloading
         self.task.cancel()
     }
}

加载器中使用DownloadOperation的代码

public func loadImage(_ urlRequest: URLRequest) async throws -> UIImage {
        if let status = images[urlRequest]{
            switch status{
            case .fetched(let image):
                return image
            case .inProgress(let task):
                return try await task.value
            case .failure(let error):
                self.hasError = true
                self.error = error as? InternetError
            }
        }
        
        
        let task: Task<UIImage, Error> = Task {
            do {
                let imageQueue = OperationQueue()
                imageQueue.maxConcurrentOperationCount = 3
                
                let operation = DownloadOperation(session: URLSession.shared, downloadTaskURL: urlRequest, completionHandler: {_, response ,_ in
                    let (imageData, response) = try await URLSession.shared.data(for: urlRequest)

                    guard let httpResponse = response as? HTTPURLResponse,
                          httpResponse.statusCode == 200 else {
                        throw InternetError.invalidServerResponse
                    }
                    guard let image = UIImage(data: imageData) else {
                        throw InternetError.noInternet
                    }
                    
                })
                imageQueue.addOperation(operation)
                
               // return image
            }
            catch {
                self.hasError = true
                images[urlRequest] = .failure(error)
                print("error caught in Loader")
                let image = UIImage(systemName: "wifi.exclamationmark")!
                return image
            }
        }
        
        do{
            images[urlRequest] = .inProgress(task)
            var image = try await task.value
            if let imageFromCache = imageCache.object(forKey: urlRequest as AnyObject) as? UIImage {
                image = imageFromCache
                return image
            }
            images[urlRequest] = .fetched(image)
            //storing image in cache
            imageCache.setObject(image, forKey: urlRequest as AnyObject)
            return image
        }
    }
}

问题分析与修复方案

1. 核心错误原因

你在DownloadOperation的completionHandler里使用了try await URLSession.shared.data(for: urlRequest),但这个completionHandler是同步闭包类型,不允许直接嵌入异步代码,这就是报错的根源。另外,你在completionHandler里重复发起了一次请求,完全没必要——DownloadOperation内部已经通过dataTask发起了请求,应该直接用这个任务返回的数据。

2. 修复DownloadOperation类

修改DownloadOperation,让它支持异步结果传递,同时正确管理操作状态:

class DownloadOperation: Operation {
    private let session: URLSession
    private let urlRequest: URLRequest
    private let completion: (Result<UIImage, Error>) -> Void
    
    private var task: URLSessionDataTask?
    
    enum OperationState: Int {
        case ready, executing, finished
    }
    
    private var state: OperationState = .ready {
        willSet {
            willChangeValue(forKey: "isExecuting")
            willChangeValue(forKey: "isFinished")
        }
        didSet {
            didChangeValue(forKey: "isExecuting")
            didChangeValue(forKey: "isFinished")
        }
    }
    
    override var isReady: Bool { state == .ready }
    override var isExecuting: Bool { state == .executing }
    override var isFinished: Bool { state == .finished }
    override var isAsynchronous: Bool { true } // 必须标记为异步操作
    
    init(session: URLSession, urlRequest: URLRequest, completion: @escaping (Result<UIImage, Error>) -> Void) {
        self.session = session
        self.urlRequest = urlRequest
        self.completion = completion
        super.init()
    }
    
    override func start() {
        if isCancelled {
            state = .finished
            completion(.failure(CancellationError()))
            return
        }
        
        state = .executing
        print("downloading")
        
        task = session.dataTask(with: urlRequest) { [weak self] data, response, error in
            guard let self = self else { return }
            
            defer {
                self.state = .finished
            }
            
            if let error = error {
                if (error as NSError).code == NSURLErrorCancelled {
                    self.completion(.failure(CancellationError()))
                } else {
                    self.completion(.failure(error))
                }
                return
            }
            
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                self.completion(.failure(InternetError.invalidServerResponse))
                return
            }
            
            guard let data = data, let image = UIImage(data: data) else {
                self.completion(.failure(InternetError.noInternet))
                return
            }
            
            self.completion(.success(image))
        }
        
        task?.resume()
    }
    
    override func cancel() {
        super.cancel()
        task?.cancel()
    }
}

3. 修复加载器函数

调整loadImage逻辑,复用OperationQueue(不要每次创建新队列),并正确处理异步结果:

// 把队列提升为类成员,避免每次创建新队列
private let imageQueue: OperationQueue = {
    let queue = OperationQueue()
    queue.maxConcurrentOperationCount = 3
    queue.name = "com.yourapp.imageDownloadQueue"
    return queue
}()

public func loadImage(_ urlRequest: URLRequest) async throws -> UIImage {
    // 先检查缓存
    if let cachedImage = imageCache.object(forKey: urlRequest as AnyObject) as? UIImage {
        images[urlRequest] = .fetched(cachedImage)
        return cachedImage
    }
    
    // 检查请求状态
    if let status = images[urlRequest] {
        switch status {
        case .fetched(let image):
            return image
        case .inProgress(let task):
            return try await task.value
        case .failure(let error):
            hasError = true
            self.error = error as? InternetError
            throw error
        }
    }
    
    // 创建异步任务包装Operation
    let task = Task<UIImage, Error> {
        try await withCheckedThrowingContinuation { continuation in
            let operation = DownloadOperation(session: URLSession.shared, urlRequest: urlRequest) { result in
                switch result {
                case .success(let image):
                    continuation.resume(returning: image)
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
            
            imageQueue.addOperation(operation)
            
            // 绑定任务取消到Operation取消
            Task { [weak operation] in
                if Task.isCancelled {
                    operation?.cancel()
                }
            }
        }
    }
    
    images[urlRequest] = .inProgress(task)
    
    do {
        let image = try await task.value
        // 缓存图片
        imageCache.setObject(image, forKey: urlRequest as AnyObject)
        images[urlRequest] = .fetched(image)
        return image
    } catch {
        hasError = true
        images[urlRequest] = .failure(error)
        print("error caught in Loader")
        // 可选:返回占位图,或者直接抛出错误
        throw error
        // return UIImage(systemName: "wifi.exclamationmark")!
    }
}

关键修复点

  • 给DownloadOperation添加isAsynchronous = true标记,这是异步Operation必须的
  • 移除completionHandler里的重复请求,直接使用dataTask返回的data和response
  • 用Result类型统一传递成功/失败结果,避免多个可选参数
  • 把imageQueue提升为类成员,确保全局并发限制生效(之前每次创建新队列等于没限制)
  • 用withCheckedThrowingContinuation把Operation的回调转换为async/await风格的异步结果
  • 绑定Task的取消到Operation的取消,确保取消逻辑一致

内容的提问来源于stack exchange,提问作者SwiftCoder

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 14:40:50