【问题标题】:SwiftUI List - Remote Image loading = images flickering (reloading)SwiftUI 列表 - 远程图像加载 = 图像闪烁(重新加载)
【发布时间】:2020-02-21 20:46:35
【问题描述】:

我是 SwiftUI 和 Combine 的新手。我有一个简单的列表,我正在加载 iTunes 数据并构建列表。加载图像有效 - 但它们在闪烁,因为似乎我在主线程上的调度一直在触发。我不确定为什么。下面是图像加载的代码,然后是它的实现位置。

struct ImageView: View {
    @ObservedObject var imageLoader: ImageLoaderNew
    @State var image: UIImage = UIImage()

    init(withURL url: String) {
        imageLoader = ImageLoaderNew(urlString: url)
    }

    func imageFromData(_ data: Data) -> UIImage {
        UIImage(data: data) ?? UIImage()
    }

    var body: some View {
        VStack {
            Image(uiImage: imageLoader.dataIsValid ?
                imageFromData(imageLoader.data!) : UIImage())
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width:60, height:60)
                .background(Color.gray)
        }
    }
}

class ImageLoaderNew: ObservableObject
{    
    @Published var dataIsValid = false
    var data: Data?

    // The dispatch fires over and over again. No idea why yet
    // this causes flickering of the images in the List. 
    // I am only loading a total of 3 items. 

    init(urlString: String) {
        guard let url = URL(string: urlString) else { return }
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data else { return }
            DispatchQueue.main.async {
                self.dataIsValid = true
                self.data = data
                print(response?.url as Any) // prints over and over again.
            }
        }
        task.resume()
    }
}

这里是加载JSON等后实现的。

List(results, id: \.trackId) { item in
    HStack {

        // All of these image end up flickering
        // every few seconds or so.
        ImageView(withURL: item.artworkUrl60)
            .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 5))

        VStack(alignment: .leading) {
            Text(item.trackName)
                .foregroundColor(.gray)
                .font(.headline)
                .truncationMode(.middle)
                .lineLimit(2)

            Text(item.collectionName)
                .foregroundColor(.gray)
                .font(.caption)
                .truncationMode(.middle).lineLimit(1)
            }
        }
    }
    .frame(height: 200.0)
    .onAppear(perform: loadData) // Only happens once

我不确定为什么图像会一遍又一遍地加载(至今)。我一定遗漏了一些简单的东西,但我绝对不知道组合的方式。任何见解或解决方案将不胜感激。

【问题讨论】:

    标签: swiftui combine


    【解决方案1】:

    我将更改分配顺序如下(因为否则dataIsValid 强制刷新,但尚未设置data

    DispatchQueue.main.async {
        self.data = data             // 1) set data
        self.dataIsValid = true.     // 2) notify that data is ready
    

    其他一切似乎都不重要。但是考虑以下优化的可能性,因为UIImage 从数据构造可能也足够长(对于 UI 线程)

    func imageFromData(_ data: Data) -> UIImage {
        UIImage(data: data) ?? UIImage()
    }
    

    所以我建议不仅将数据,而且将整个图像构造移动到后台线程中,并仅在最终图像准备好时刷新。

    更新:我已使您的代码快照在本地工作,当然,由于并非所有部分最初都可用,因此进行了一些替换和简化,并建议进行上述修复。以下是一些实验结果:

    如您所见,没有观察到闪烁,也没有重复登录控制台。由于我没有对您的代码逻辑进行任何重大更改,我认为导致报告闪烁的问题不在提供的代码中 - 可能其他一些部分导致重新创建 ImaveView 从而产生这种效果。

    这是我的代码(完全,用 Xcode 11.2/3 和 iOS 13.2/3 测试):

    struct ImageView: View {
        @ObservedObject var imageLoader: ImageLoaderNew
        @State var image: UIImage = UIImage()
    
        init(withURL url: String) {
            imageLoader = ImageLoaderNew(urlString: url)
        }
    
        func imageFromData(_ data: Data) -> UIImage {
            UIImage(data: data) ?? UIImage()
        }
    
        var body: some View {
            VStack {
                Image(uiImage: imageLoader.dataIsValid ?
                    imageFromData(imageLoader.data!) : UIImage())
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width:60, height:60)
                    .background(Color.gray)
            }
        }
    }
    
    class ImageLoaderNew: ObservableObject
    {
        @Published var dataIsValid = false
        var data: Data?
    
        // The dispatch fires over and over again. No idea why yet
        // this causes flickering of the images in the List.
        // I am only loading a total of 3 items.
    
        init(urlString: String) {
            guard let url = URL(string: urlString) else { return }
            let task = URLSession.shared.dataTask(with: url) { data, response, error in
                guard let data = data else { return }
                DispatchQueue.main.async {
                    self.data = data
                    self.dataIsValid = true
                    print(response?.url as Any) // prints over and over again.
                }
            }
            task.resume()
        }
    }
    
    struct TestImageFlickering: View {
        @State private var results: [String] = []
        var body: some View {
            NavigationView {
                List(results, id: \.self) { item in
                    HStack {
    
                        // All of these image end up flickering
                        // every few seconds or so.
                        ImageView(withURL: item)
                            .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 5))
    
                        VStack(alignment: .leading) {
                            Text("trackName")
                                .foregroundColor(.gray)
                                .font(.headline)
                                .truncationMode(.middle)
                                .lineLimit(2)
    
                            Text("collectionName")
                                .foregroundColor(.gray)
                                .font(.caption)
                                .truncationMode(.middle).lineLimit(1)
                        }
                    }
                }
                .frame(height: 200.0)
                .onAppear(perform: loadData)
            } // Only happens once
        }
    
        func loadData() {
            var urls = [String]()
            for i in 1...10 {
                urls.append("https://placehold.it/120.png&text=image\(i)")
            }
            self.results = urls
        }
    }
    
    struct TestImageFlickering_Previews: PreviewProvider {
        static var previews: some View {
            TestImageFlickering()
        }
    }
    

    【讨论】:

    • 我切换了顺序 - 它仍然被一遍又一遍地调用(print(response?.url as Any)。其他事情正在发生。
    • @Asperi 更改顺序无济于事,但“我建议不仅将数据,而且将整个图像构造移动到后台线程并仅在最终图像准备好时刷新”可能会有所帮助,如果最终图像将被创建为固定大小
    【解决方案2】:
    // The dispatch fires over and over again. No idea why yet
    // this causes flickering of the images in the List. 
    // I am only loading a total of 3 items. 
    
        init(urlString: String) { ....
    

    表示init一遍遍地调用

    只能从这部分调用

    // All of these image end up flickering
    // every few seconds or so.
        ImageView(withURL: item.artworkUrl60)
            .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 5))
    

    在什么情况下 SwiftUI 会这样做?除了改变布局,我什么也没看到。看来,调整 ImageView 的大小是发现这种闪烁的地方。

    我会尝试玩这部分

    .resizable()
    .aspectRatio(contentMode: .fit)
    

    或申请

    ImageView(withURL: item.artworkUrl60)
        .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 5))
        .fixedSize()
    

    Asperi 的建议(从后台下载的数据创建固定大小的图像)并删除

    .resizable()
    .aspectRatio(contentMode: .fit)
    

    从你的布局来看,可能是最好的方法

    或者只是从 ImageView 中移动 .frame(width:60, height:60) 并将其应用到您的列表中

    ImageView(withURL: item.artworkUrl60)
        .frame(width:60, height:60)
        .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 5))
    

    我还将尝试修复所有 List 组件的大小...,直到您找到哪个使闪烁。

    【讨论】:

      【解决方案3】:

      我在使用的 SwiftUI 远程图像加载器中遇到了类似的问题。尝试使 ImageView 相等,以避免在 uiImage 设置为初始化的 nil 值以外的值或返回 nil 后重绘。

      struct ImageView: View, Equatable {
      
          static func == (lhs: ImageView, rhs: ImageView) -> Bool {
              let lhsImage = lhs.image
              let rhsImage = rhs.image
              if (lhsImage == nil && rhsImage != nil) || (lhsImage != nil && rhsImage == nil) {
                  return false
              } else {
                  return true
              }
          }
      
          @ObservedObject var imageLoader: ImageLoaderNew
          @State var image: UIImage?
      
      // ... rest the same
      

      【讨论】:

        【解决方案4】:

        我在处理远程图像时遇到了类似的问题。

        我使用像你这样的 ObservableObject 作为图像加载器,发现这就是问题所在。 SwiftUI 会多次创建 ImageView 结构,如果您实例化一个新的 ImageLoader 并在每次最终收到数百个 URLSession 请求时加载数据。即使网络请求被缓存,你也必须将 Data 转换为 UIImage 数百次。

        我通过在单独的类中实现缓存机制来解决闪烁问题。 我仍然使用 ImageLoader,所以我可以为我的 SwiftUI 视图创建一个 ObservableObject,但我没有在那里创建 URLSession 数据任务,我从不同的对象获取 UIImage 并且 UIImage 本身被缓存,而不是来自网络请求的数据.

        class ImageLoader: ObservableObject {
            @Published var image = UIImage()
            
            func load(url:URL) {
                loadImage(fromURL: url)
            }
            
            func load(urlString:String) {
                guard let url = URL(string: urlString) else { return }
                loadImage(fromURL: url)
            }
            
            // MARK: - Private
            private var imageCache = ImageCache.shared
            
            private func loadImage(fromURL url:URL) {
                imageCache.imageForURL(url) { image in
                    if let image = image {
                        DispatchQueue.main.async {
                            self.image = image
                        }
                    }
                }
            }
        }
        

        而实际的图片加载是在另一个类中完成的

        typealias ImageCacheCompletion = (UIImage?) -> Void
        
        class ImageCache {
            static let shared = ImageCache()
            
            func imageForURL(_ url:URL, completion: @escaping ImageCacheCompletion) {
                if let cachedImage = cache.first(where: { record in
                    record.urlString == url.absoluteString
                }) {
                    completion(cachedImage.image)
                }
                else {
                    loadImage(url: url, completion: completion)
                }
            }
            
            // MARK: - Private
            private var cache:[ImageCacheRecord] = []
            private let cacheSize = 10
            
            private func imageFromData(_ data:Data) -> UIImage? {
                UIImage(data: data)
            }
            
            private func loadImage(url:URL, completion: @escaping ImageCacheCompletion) {
                RESTClient.loadData(atURL: url) { result in
                    switch result {
                    case .success(let data):
                        if let image = self.imageFromData(data) {
                            completion(image)
                            self.setImage(image, forUrl: url.absoluteString)
                        }
                        else {
                            completion(nil)
                        }
                    case .failure(_ ):
                        completion(nil)
                    }
                }
            }
            
            private func setImage(_ image:UIImage, forUrl url:String) {
                if cache.count >= cacheSize {
                    cache.remove(at: 0)
                    
                }
                cache.append(ImageCacheRecord(image: image, urlString: url))
            }
            
            // MARK: - ImageCacheRecord
            
            struct ImageCacheRecord {
                var image:UIImage
                var urlString:String
            }
        }
        

        这个实现的一个例子是GitHub

        您可能需要调整一些参数,例如缓存大小以满足您的需求,正如您所见,我使用 FIFO 方法,因此缓存类有改进的空间,但这是一个很好的起点。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-12-23
          • 2016-03-17
          • 2020-08-04
          • 2014-01-10
          • 2013-01-20
          • 2011-03-05
          • 1970-01-01
          相关资源
          最近更新 更多