Why Doesn't Instantiation In SwiftUI @main App Not Create An EnvironmentObject?

Viewed 59

I am working with SwiftUI and @EnvironmentObjects. I am using the SwiftUI App Lifecycle. In this file I create a @StateObjects for ListingRepository and attach it to ContentView() with .envrionmentObjects().

struct MyApp: App {
    // @EnvironmentObjects
    @StateObject private var listingRepository = ListingRepository()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(listingRepository)
        }
    }
}

My assumption was that I could now access listingRepository by using an @EnvironmentObject wrapper. However, it seems I must instantiate this again. Below is my ListingViewModel. My first attempt looked like the following.

class MarketplaceViewModel: ObservableObject {
    @EnvironmentObject var listingRepository: ListingRepository
    @Published var listingRowViewModels = [ListingRowViewModel]()
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        listingRepository
            .$listings
            .receive(on: RunLoop.main)
            .map { listings in
                listings.map { listing in
                    ListingRowViewModel(listing: listing)
                }
            }
            .assign(to: \.listingRowViewModels, on: self)
            .store(in: &cancellables)
    }
}

This threw the following error.

Fatal error: No ObservableObject of type ListingRepository found. A View.environmentObject(_:) for ListingRepository may be missing as an ancestor of this view.

The second option uses @Published and fixes the error.

class MarketplaceViewModel: ObservableObject {
    @Published var listingRepository = ListingRepository()
    @Published var listingRowViewModels = [ListingRowViewModel]()
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        listingRepository
            .$listings
            .receive(on: RunLoop.main)
            .map { listings in
                listings.map { listing in
                    ListingRowViewModel(listing: listing)
                }
            }
            .assign(to: \.listingRowViewModels, on: self)
            .store(in: &cancellables)
    }
}

My questions are the following.

  1. Is it necessary to instantiate twice like I am?
  2. Why doesn't the instantiation in @main App work?
1 Answers

in your @main App you should define your environment object like this:

struct MyApp: App {
    // @EnvironmentObjects
    var listingRepository = ListingRepository()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(listingRepository)
        }
    }
}

Then it suffices to have the ListingRepository define as:

class ListingRepository: ObservableObject {

    @Published var ...
    @Published var ...

    // Your code here
}

Don't redeclare the ListingRepository as an EnvironmentObject in your MarketPlaceViewModel.

One note, however, it is not wise to make your ListingRepository an observable object and access this object from your views through the MarketPlaceViewModel. If the MarketPlaceViewModel is used to fill the views in your app and the MarketPlaceViewModel gets the data from the ListingRepository, you should make your MarketPlaceViewModel the EnvironmentObject and not the ListingRepository.

The thing in SwiftUI is the you want the EnvironmentObject to publish changes to several views in your app, so that these views can reconstruct themselves. If you use a view model between your model and your view, the view model should be the EnvironmentObject.

Related