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.
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
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:
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()
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With