SwiftUI中URLSession限制请求数遇异步转换错误求助
我正在实现基于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

