How do I implement @StateObject in AsyncImage?

Hello, I had a lab session earlier today to try and handle an issue with images getting dumped in SwiftUI and always seeing the placeholder image instead of the loaded image. The engineer recommended @StateObject as a replacement for @ObservedObject in the AsyncImage but Im getting an error.
Is there a recommended way to use this new feature of SwiftUI for Async Images?

With @StateObject in place of @ObservedObject 'loader' below throws error "Cannot assign to property: 'loader' is a get-only property"

Code Block
struct AsyncImage<Placeholder: View>: View {
    @ObservedObject private var loader: ImageLoader
    private let placeholder: Placeholder?
    init(url: URL, placeholder: Placeholder? = nil) {
        loader = ImageLoader(url: url)
        self.placeholder = placeholder
    }
    var body: some View {
        image
            .onAppear(perform: loader.load)
            .onDisappear(perform: loader.cancel)
    }
    private var image: some View {
        Group {
            if loader.image != nil {
                Image(uiImage: loader.image!)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else {
                Image("Placeholder")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            }
        }
    }
}

Thanks!
Answered by OOPer in 616924022
I'm afraid you do not understand what I say.

You need to initialize loader at the place of declaration.

Writing this way is mandatory.
Code Block
    @StateObject private var loader = ImageLoader() //<- You need this here.


Also, you need to check if your ImageLoader has an initializer with no arguments -- init(). Explicitly defined or implicitly generated.
I think you need to initialize loader at the place of declaration.
Code Block
struct AsyncImage<Placeholder: View>: View {
    @StateObject private var loader = ImageLoader()
    private let placeholder: Placeholder?
    private let url: URL?
    init(url: URL, placeholder: Placeholder? = nil) {
        self.placeholder = placeholder
        self.url = url
    }
    var body: some View {
        image
            .onAppear{
                loader.url = url
                loader.load()
            }
            .onDisappear(perform: loader.cancel)
    }
//...
}

You may need to update your ImageLoader.

You'll have to change how ImageLoader is initialized as well. Make it default constructed and stored in a @StateObject, which it sounds like you already did. Next, instead of overwriting the state object in the view's init, delete the init entirely, and instead change the .onAppear() load call to pass the url.

Here's a good antipattern rule to recognize: if you want to do something only once per view, it shouldn't be in the view's init. SwiftUI is creating a new flavor of the same view many times in reaction to events.
Okay, I'll be honest I'm pretty new at this and a lot of what is described is beyond my understanding! Do I have to initialize separately where the AsyncImage is called in my views? And do I have to change the init in my ImageLoader also?
Code Block
class ImageLoader: ObservableObject {
    @Published var image: UIImage?
    private let url: URL
    private var cancellable: AnyCancellable?
    deinit {
        cancellable?.cancel()
    }
    init(url: URL) {
        self.url = url
    }
    func load() {
        cancellable = URLSession.shared.dataTaskPublisher(for: url)
            .map { UIImage(data: $0.data) }
            .replaceError(with: nil)
            .receive(on: DispatchQueue.main)
            .assign(to: \.image, on: self)
    }
    func cancel() {
        cancellable?.cancel()
    }
}
struct AsyncImage<Placeholder: View>: View {
    @StateObject private var loader: ImageLoader
    private let placeholder: Placeholder?
    init(url: URL, placeholder: Placeholder? = nil) {
        loader = ImageLoader(url: url)
        self.placeholder = placeholder
    }
    var body: some View {
        image
            .onAppear(perform: loader.load)
            .onDisappear(perform: loader.cancel)
    }
    private var image: some View {
        Group {
            if loader.image != nil {
                Image(uiImage: loader.image!)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else {
                Image("BlankCardImage")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            }
        }
    }


@Oghweb 

Do I have to initialize separately where the AsyncImage is called in my views? And do I have to change the init in my ImageLoader also?

One thing sure is that you need to initialize loader at the place of declaration. All other changes may be needed depending on it.

Code Block
class ImageLoader: ObservableObject {
    @Published var image: UIImage?
    var url: URL?
    private var cancellable: AnyCancellable?
    deinit {
        cancellable?.cancel()
    }
    func load() {
        guard let url = url else {return}
        cancellable = URLSession.shared.dataTaskPublisher(for: url)
            .map { UIImage(data: $0.data) }
            .replaceError(with: nil)
            .receive(on: DispatchQueue.main)
            .assign(to: \.image, on: self)
    }
    func cancel() {
        cancellable?.cancel()
    }
}
struct AsyncImage<Placeholder: View>: View {
    @StateObject private var loader = ImageLoader() //<- You need this here.
    private let placeholder: Placeholder?
    private let url: URL?
    init(url: URL, placeholder: Placeholder? = nil) {
        self.placeholder = placeholder
        self.url = url
    }
    var body: some View {
        image
            .onAppear{
                loader.url = url
                loader.load()
            }
            .onDisappear(perform: loader.cancel)
    }
    private var image: some View {
        Group {
            if loader.image != nil {
                Image(uiImage: loader.image!)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else {
                Image("Placeholder")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            }
        }
    }
}



Thanks @OOPer! I think I'm getting closer. Here's how I'm using the Async image in my view, but the errors aren't any help.

Code Block
                AsyncImage(
                    loader: ImageLoader()
                    url: URL(string: card.images!["normal"]!)!,
                    placeholder: Text("")
                )

I'm at a loss where/how to initialize it. The error reads "'ImageLoader' cannot be constructed because it has no accessible initializers"
Accepted Answer
I'm afraid you do not understand what I say.

You need to initialize loader at the place of declaration.

Writing this way is mandatory.
Code Block
    @StateObject private var loader = ImageLoader() //<- You need this here.


Also, you need to check if your ImageLoader has an initializer with no arguments -- init(). Explicitly defined or implicitly generated.
How do I implement @StateObject in AsyncImage?
 
 
Q