SwiftUI列表-远程图像加载=图像闪烁(重新加载)

时间:2020-02-21 20:46:35

标签: swiftui combine

我是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

我不确定为什么图像会一遍又一遍地加载(至今)。我肯定想念一些简单的东西,但是对于组合的方式,我绝对不明智。任何见解或解决方案将不胜感激。

4 个答案:

答案 0 :(得分: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()
}

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

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

enter image description here

如您所见,没有观察到闪烁,也没有在控制台中重复登录。由于我没有对您的代码逻辑进行任何重大更改,因此我认为导致所报告的闪烁的问题不在提供的代码中-可能还有其他部分导致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()
    }
}

答案 1 :(得分:0)

// 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)

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

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

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

我还将尝试固定所有List组件的大小...,直到找到导致闪烁的组件。

答案 2 :(得分:0)

我正在使用的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

答案 3 :(得分:0)

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

我使用像您这样的 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 方法,因此缓存类还有改进的空间,但这是一个很好的起点。