2

I have a view that displays a few photos that are loaded from an API in a scroll view. I want to defer fetching the images until the view is displayed. My view, simplified looks something like this:

struct DetailView : View {
    @ObservedObject var viewModel: DetailViewModel

    init(viewModel: DetailViewModel) {
        self.viewModel = viewModel
    }

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Color("peachLight").edgesIgnoringSafeArea(.all)
                if self.viewModel.errorMessage != nil {
                    ErrorView(error: self.viewModel.errorMessage!)
                } else if self.viewModel.imageUrls.count == 0 {
                    VStack {
                        Text("Loading").foregroundColor(Color("blueDark"))
                        Text("\(self.viewModel.imageUrls.count)").foregroundColor(Color("blueDark"))
                    }
                } else {
                    VStack {
                        UIScrollViewWrapper {
                            HStack {
                                ForEach(self.viewModel.imageUrls, id: \.self) { imageUrl in
                                    LoadableImage(url: imageUrl)
                                        .scaledToFill()
                                }.frame(width: geometry.size.width, height: self.scrollViewHeight)
                            }.edgesIgnoringSafeArea(.all)
                        }.frame(width: geometry.size.width, height: self.scrollViewHeight)
                        Spacer()
                    }
                }
            }
        }.onAppear(perform: { self.viewModel.fetchDetails() })
            .onReceive(viewModel.objectWillChange, perform: {
                print("Received new value from view model")
                print("\(self.viewModel.imageUrls)")
            })
    }
}

my view model looks like this:

import Foundation
import Combine

class DetailViewModel : ObservableObject {
    @Published var imageUrls: [String] = []
    @Published var errorMessage : String?

    private var fetcher: Fetchable
    private var resourceId : String


    init(fetcher: Fetchable, resource: Resource) {
        self.resourceId = resource.id
        // self.fetchDetails() <-- uncommenting this line results in onReceive being called + a view update
    }


    // this is a stubbed version of my data fetch that performs the same way as my actual
    // data call in regards to ObservableObject updates

    // MARK - Data Fetching Stub
    func fetchDetails() {
        if let path = Bundle.main.path(forResource: "detail", ofType: "json") {
            do {
                let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
                let parsedData = try JSONDecoder().decode(DetailResponse.self, from: data)
                self.imageUrls = parsedData.photos // <-- this doesn't trigger a change, and even manually calling self.objectWillChange.send() here doesn't trigger onReceive/view update
                print("setting image urls to \(parsedData.photos)")

            } catch  {
                print("error decoding")
            }
        }
    }
}

If I fetch my data within the init method of my view model, the onReceive block on my view IS called when the @Published imageUrls property is set. However, when I remove the fetch from the init method and call from the view using:

 .onAppear(perform: { self.viewModel.fetchDetails() })

the onReceive for viewModel.objectWillChange is NOT called, even though the data is updated. I don't know why this is the case and would really appreciate any help here.

Lauren
  • 281
  • 2
  • 7

2 Answers2

5

Use instead

.onReceive(viewModel.$imageUrls, perform: { newUrls in
    print("Received new value from view model")
    print("\(newUrls)")
})
Asperi
  • 228,894
  • 20
  • 464
  • 690
0

I tested this as I found the same issue, and it seems like only value types can be used with onReceive

use enums, strings, etc.

it doesn't work with reference types because I guess technically a reference type doesn't change reference location and simply points elsewhere when changed? idk haha but ya

as a solution, you can set a viewModel @published property which is like a state enum, make changes to that when you have new data, and then on receive can access that...hope that makes sense, let me know if not

Arjun Patel
  • 79
  • 1
  • 5