Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI List - Remote Image loading = images flickering (reloading)

I am new to SwiftUI and also Combine. I have a simple List and I am loading iTunes data and building the list. Loading of the images works - but they are flickering, because it seems my dispatch on the main thread keeps firing. I am not sure why. Below is the code for the image loading, followed by where it's implemented.

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()
    }
}

And here it's implemented after loading the JSON, etc.

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

I am not sure why the images keep loading over and over again (yet). I must be missing something simple, but I am definitely not wise to the ways of Combine quite yet. Any insight or solution would be much appreciated.

like image 478
Radagast the Brown Avatar asked Sep 17 '25 18:09

Radagast the Brown


2 Answers

I was facing a similar issue in a SwiftUI remote image loader I was working on. Try making ImageView equatable to avoid redrawing after the uiImage has been set to something other than the initialized nil value or if it goes back to 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
like image 72
Cenk Bilgen Avatar answered Sep 22 '25 04:09

Cenk Bilgen


I would change the order of assigning as below (because otherwise dataIsValid force refresh, but data is not set yet)

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

everything else does not seem important. However consider possibility to optimise below, because UIImage construction from data might be also long enough (for UI thread)

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

so I would recommend to move not only data, but entire image construction into background thread and refresh only when final image is ready.

Update: I've made your code snapshot work locally, with some replacements and simplifications of course, due to not all parts originally available, and proposed above fix. Here is some experimenting result:

enter image description here

as you see no flickering is observed and no repeated logging in console. As I did not made any major changes in you code logic I assume the issue resulting in reported flickering is not in provided code - probably some other parts cause recreation of ImaveView that gives that effect.

Here is my code (completely, tested with 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()
    }
}
like image 33
Asperi Avatar answered Sep 22 '25 05:09

Asperi