2

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.

2 Answers 2

3

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)
        }
    }
}
Sign up to request clarification or add additional context in comments.

6 Comments

thanks again for the response :D I don t think it s possible to do this way. cannot unwrap the data in: .onReceive(Just(imageLoader.$data)) { data in guard let data = data else { return } self.image = UIImage(data: data) ?? UIImage() } Initializer for conditional binding must have Optional type, not 'Published<Data?>.Publisher'
@AdrianMacarenco Apologies, I fixed my answer. But you can try using the onChange modifier as well.
no, it's not working. But I do not undestand, why is it notworking...and what is the difference if I use a breakpoint and then continue running the project.
My target is iOS 13, and it's complaining about the target. :( But it's hard to understand why is it not working properly, why if I use the break point it changes the image. Is there a why of doing that for iOS 13?
@AdrianMacarenco Yeah, I fixed all issues and tested both versions. Please see the updated answer.
|
2

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.