3

I have a Game-object that may hold an image. Whenever an image URL is found for a game a new instance of GameImage-object should be created. It will then fetch the image and populate the UIImage property. When this happens the UI should be updated presenting the image.

class Game: ObservableObject {
    @Published var image: GameImage?
} 

class GameImage: ObservableObject {
    let url: URL
    @Published var image: UIImage?
    
    private var cancellable: AnyCancellable?
    
    init(url: URL) {
        self.url = url
    }
    
    func fetch() {
        self.cancellable = URLSession.shared.dataTaskPublisher(for: self.url)
            .map { UIImage(data: $0.data) }
            .replaceError(with: nil)
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { [weak self] (image) in
                guard let self = self else { return }
                self.image = image
                print(self.url)
                print(self.image)
            })
    }
    
    func cancel() {
        cancellable?.cancel()
    }
    
    deinit {
        cancel()
    }
}

struct ContentView: View {
    
    @StateObject var game = Game()

    var body: some View {
        VStack {
            if let image = game.image?.image {
                Image(uiImage: image)
            } else {
                Text("No image.")
            }
        }
        .onAppear(perform: {
            guard let gameImageURL = URL(string: "https://cf.geekdo-images.com/itemrep/img/oVEpcbtyWkJjIjk1peTJo6hI1yk=/fit-in/246x300/pic4884996.jpg") else { return }
            game.image = GameImage(url: gameImageURL)
            game.image!.fetch()
        })
    }
}

The problem is. After fetch is done the debug console will show that image contains an UIImage. However the UI does not update to show the image. What am I missing here?

2
  • Would you show the UI dependent on this view model? Commented Aug 15, 2020 at 12:28
  • I am not sure what you mean. The Game-instance game should contain the single source of truth. The Game-object can also contain other properties like name which I removed from this sample to not over complicate things. Whenever an image comes available it should present it, in that way it is dependent. Commented Aug 15, 2020 at 12:37

1 Answer 1

1

There is much more simpler solution than chaining ObservableObject, just separate dependent part into standalone subview... and all will work automatically.

Here is possible approach. Tested with Xcode 12 / iOS 14.

struct ContentView: View {

    @StateObject var game = Game()

    var body: some View {
        VStack {
            if nil != game.image {
                GameImageView(vm: game.image!)
            }
        }
        .onAppear(perform: {
            guard let gameImageURL = URL(string: "https://cf.geekdo-images.com/itemrep/img/oVEpcbtyWkJjIjk1peTJo6hI1yk=/fit-in/246x300/pic4884996.jpg") else { return }
            game.image = GameImage(url: gameImageURL)
            game.image!.fetch()
        })
    }
}

struct GameImageView: View {
    @ObservedObject var vm: GameImage

    var body: some View {
        if let image = vm.image {
            Image(uiImage: image)
        } else {
            Text("No image.")
        }
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

Isn't it better not to force-unwrap optionals? Can't it be game.image?.fetch()? I know this was in the question but you can always improve it.
Yes, although in this example I set the game.image one line above so it's pretty safe.
I just tested the code and it worked. I am still trying to wrap my head around the concept.
@Mark Sure, but as a good practice I recommend to not to force-unwrap optionals. You may copy this line somewhere else later and then forget about it.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.