SwiftUI中URLImage在List内因视图销毁导致任务取消的问题修复咨询
嘿,我懂你遇到的这个糟心问题!List滚动的时候,行视图会被频繁创建和销毁,你的URLImage里的ImageLoader跟着视图一起被释放,加载任务自然就被取消,导致控制台一直刷"cancelled"的错误,太影响开发体验了。
先帮你理清楚核心原因:你现在把ImageLoader作为URLImage的普通实例属性,每次URLImage被List创建出来,都会生成一个新的ImageLoader;而当List滚动回收行视图时,对应的URLImage实例被销毁,ImageLoader也跟着被释放,它正在进行的URLSession数据任务就会被直接取消——这就是你看到"cancelled"错误的根源。
下面给你几个实用的修复方案,你可以根据自己的需求选:
方案一:用@StateObject绑定ImageLoader的生命周期
把ImageLoader改成遵守ObservableObject的类,然后用*@StateObject*来持有它,这样ImageLoader的生命周期会和URLImage绑定,即使List的行视图被复用,ImageLoader实例也不会被轻易销毁,加载任务能正常完成,同时加入内存缓存避免重复加载:
首先修改ImageLoader:
class ImageLoader: ObservableObject { @Published var image: UIImage? private var task: URLSessionDataTask? private let cache = NSCache<NSURL, UIImage>() // 内存缓存 func loadImage(from url: URL) { // 先检查缓存,有就直接返回 if let cachedImage = cache.object(forKey: url as NSURL) { self.image = cachedImage return } // 取消之前的任务(如果有的话) task?.cancel() task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in guard let self = self else { return } if let error = error { // 过滤取消错误,避免控制台刷屏 if (error as NSError).code != NSURLErrorCancelled { print("加载错误:\(error.localizedDescription)") } return } guard let data = data, let loadedImage = UIImage(data: data) else { print("图片数据无效") return } // 存入缓存 self.cache.setObject(loadedImage, forKey: url as NSURL) DispatchQueue.main.async { self.image = loadedImage } } task?.resume() } // 销毁时取消任务 deinit { task?.cancel() } }
然后修改URLImage:
struct URLImage: View { let url: URL? @StateObject private var imageLoader = ImageLoader() // 用@StateObject持有加载器 var body: some View { Image(uiImage: imageLoader.image ?? UIImage(systemName: "photo")!) .resizable() .onAppear { guard let url = url else { return } imageLoader.loadImage(from: url) } } }
这个方案的好处是每个URLImage实例都有独立的加载器,但通过@StateObject保证了加载器的生命周期和视图一致,同时缓存能避免同一URL的图片重复加载。
方案二:全局单例ImageLoader+共享缓存
如果希望整个APP内的图片加载都共享缓存和任务管理,可以把ImageLoader做成单例,这样即使URLImage被销毁,加载任务还能在单例里继续执行,加载完成后缓存起来,下次URLImage创建时直接取缓存:
class ImageLoader: ObservableObject { static let shared = ImageLoader() // 单例实例 @Published private var imageCache: [URL: UIImage] = [:] private var tasks: [URL: URLSessionDataTask] = [:] private init() {} // 私有初始化,保证单例唯一性 func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) { // 先检查缓存 if let cachedImage = imageCache[url] { completion(cachedImage) return } // 如果已有任务在加载,等待结果 if tasks[url] != nil { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.loadImage(from: url, completion: completion) } return } let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in guard let self = self else { return } defer { self.tasks.removeValue(forKey: url) } if let error = error { // 过滤取消错误 if (error as NSError).code != NSURLErrorCancelled { print("加载错误:\(error.localizedDescription)") } completion(nil) return } guard let data = data, let loadedImage = UIImage(data: data) else { print("图片数据无效") completion(nil) return } DispatchQueue.main.async { self.imageCache[url] = loadedImage completion(loadedImage) } } tasks[url] = task task?.resume() } }
然后修改URLImage:
struct URLImage: View { let url: URL? @State private var image: UIImage? var body: some View { Image(uiImage: image ?? UIImage(systemName: "photo")!) .resizable() .onAppear { guard let url = url else { return } if image == nil { ImageLoader.shared.loadImage(from: url) { [weak self] loadedImage in self?.image = loadedImage } } } } }
这个方案的优势是全局共享缓存,即使多个URLImage加载同一URL,也只会发起一次请求,而且任务不会因为单个URLImage的销毁而被取消,大大提升了加载效率。
额外小技巧:过滤取消错误
不管用哪个方案,你都可以在错误处理时过滤掉NSURLErrorCancelled错误,这样控制台就不会一直刷"cancelled"的日志了,判断方式如下:
if let nsError = error as NSError?, nsError.code == NSURLErrorCancelled { // 忽略取消错误 return }
这样就不会再看到烦人的"cancelled"提示啦。
内容来源于stack exchange

