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

SwiftUI中URLImage在List内因视图销毁导致任务取消的问题修复咨询

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 13:13:02