Adrian Macarenco
Adrian Macarenco

Reputation: 175

SwiftUI Image from URL not showing

I have a problem when I want to show an Image from a URL. I created a class for downloading data and publishing the data forward - ImageLoader:

class ImageLoader: ObservableObject {
    var didChange = PassthroughSubject<Data, Never>()
    var data = Data() {
        didSet {
            didChange.send(data)
        }
    }
    
    func loadData(from urlString: String?) {
        if let urlString = urlString {
            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
                }
            }
            task.resume()
        }
    }
}

Therefore, I use it inside a ImageView struct which I use inside my screen.

struct ImageView: View {
    var urlString: String
    @ObservedObject var imageLoader: ImageLoader = ImageLoader()
    @State var image: UIImage = UIImage(named: "homelessDogsCats")!

    var body: some View {
        ZStack() {
            Image(uiImage: image)
                .resizable()
                .onReceive(imageLoader.didChange) { data in
                    self.image = UIImage(data: data) ?? UIImage()
            }
        }.onAppear {
            self.imageLoader.loadData(from: urlString)
        }
    }
}

My problem is that if I just run my project, the image doesn't change and by default appears only image UIImage(named: "homelessDogsCats").

If I add a breakpoint inside

onAppear { 
    self.imageLoader.loadData(from: urlString) 
}

and just step forward, the image is showing.

I have the same problem in another view which usually doesn't display the Image from URL, but sometimes it does.

Upvotes: 2

Views: 7523

Answers (2)

Naishta
Naishta

Reputation: 12383

Working version for iOS 13, 14 (AsyncImage 1 liner is introduced in iOS 15, and the solution below is for the versions prior to that in case your min deployment target is not iOS 15 yet ) and with the latest property wrappers - Observed, Observable and Publisher ( without having to use PassthroughSubject<Data, Never>()

Main View

import Foundation
import SwiftUI
import Combine

struct TransactionCardRow: View {
    var transaction: Transaction

    var body: some View {
        CustomImageView(urlString: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png") // This is where you extract urlString from Model ( transaction.imageUrl)
    }
}

Creating CustomImageView

struct CustomImageView: View {
    var urlString: String
    @ObservedObject var imageLoader = ImageLoaderService()
    @State var image: UIImage = UIImage()
    
    var body: some View {
        Image(uiImage: image)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width:100, height:100)
            .onReceive(imageLoader.$image) { image in
                self.image = image
            }
            .onAppear {
                imageLoader.loadImage(for: urlString)
            }
    }
}

Creating a service layer to download the Images from url string, using a Publisher

class ImageLoaderService: ObservableObject {
    @Published var image: UIImage = UIImage()
    
    func loadImage(for 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.image = UIImage(data: data) ?? UIImage()
            }
        }
        task.resume()
    }
    
}

Upvotes: 2

pawello2222
pawello2222

Reputation: 54611

Try using @Published - then you don't need a custom PassthroughSubject:

class ImageLoader: ObservableObject {
    // var didChange = PassthroughSubject<Data, Never>() <- remove this
    @Published var data: Data?
    ...
}

and use it in your view:

struct ImageView: View {
    var urlString: String
    @ObservedObject var imageLoader = ImageLoader()
    @State var image = UIImage(named: "homelessDogsCats")!

    var body: some View {
        ZStack() {
            Image(uiImage: image)
                .resizable()
                .onReceive(imageLoader.$data) { data in
                    guard let data = data else { return }
                    self.image = UIImage(data: data) ?? UIImage()
                }
        }.onAppear {
            self.imageLoader.loadData(from: urlString)
        }
    }
}

Note: if you're using SwiftUI 2, you can use @StateObject instead of @ObservedObject and onChange instead of onReceive.

struct ImageView: View {
    var urlString: String
    @StateObject var imageLoader = ImageLoader()
    @State var image = UIImage(named: "homelessDogsCats")!

    var body: some View {
        ZStack() {
            Image(uiImage: image)
                .resizable()
                .onChange(of: imageLoader.data) { data in
                    guard let data = data else { return }
                    self.image = UIImage(data: data) ?? UIImage()
                }
        }.onAppear {
            self.imageLoader.loadData(from: urlString)
        }
    }
}

Upvotes: 3

Related Questions