2

I am working on a SwiftUI Project using MVVM. I have the following files for a marketplace that has listings.

ListingRepository.swift - Connecting to Firebase Firestore Listing.swift - Listing Model File MarketplaceViewModel - Marketplace View Model MarketplaceView - List view of listings for the marketplace

Originally, I was making my repository file the EnvironmentObject which worked. While researching I am realizing it makes more sense to make the ViewModel the EnvironmentObject. However, I am having trouble making an EnvironmentObject. Xcode is giving me the following error in my MarketplaceView.swift file when I try and access marketplaceViewModel and I can't understand why?

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

Here are the files in a simplified form.

App File

@main struct Global_Seafood_ExchangeApp: App { 

@StateObject private var authSession = AuthSession() @StateObject private var marketplaceViewModel = MarketplaceViewModel(listingRepository: ListingRepository())

 var body: some Scene { WindowGroup { ContentView() .environmentObject(marketplaceViewModel) 

.environmentObject(authSession) } } }

ListingRepository.swift

class ListingRepository: ObservableObject { let db = Firestore.firestore() @Published var listings = [Listing]() init() { startSnapshotListener() } func startSnapshotListener() { db.collection(FirestoreCollection.listings).addSnapshotListener { (querySnapshot, error) in if let error = error { print("Error getting documents: \(error)") } else { guard let documents = querySnapshot?.documents else { print("No Listings.") return } self.listings = documents.compactMap { listing in do { return try listing.data(as: Listing.self) } catch { print(error) } return nil } } } } } 

Listing.swift

struct Listing: Codable, Identifiable { @DocumentID var id: String? var title: String? } 

MarketplaceModelView.swift

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

MarketplaceView.swift

struct MarketplaceView: View { @EnvironmentObject var marketplaceViewModel: MarketplaceViewModel var body: some View { // ERROR IS HERE Text(self.marketplaceViewModel.listingRowViewModels[1].listing.title) } } 

ListingRowViewModel.swift

class ListingRowViewModel: ObservableObject { var id: String = "" @Published var listing: Listing private var cancellables = Set<AnyCancellable>() init(listing: Listing) { self.listing = listing $listing .receive(on: RunLoop.main) .compactMap { listing in listing.id } .assign(to: \.id, on: self) .store(in: &cancellables) } } 

ContentView.swift

struct ContentView: View { @EnvironmentObject var authSession: AuthSession @EnvironmentObject var marketplaceViewModel: MarketplaceViewModel var body: some View { Group{ if (authSession.currentUser != nil) { TabView { MarketplaceView() .tabItem { Image(systemName: "shippingbox") Text("Marketplace") }.tag(0) // MarketplaceView AccountView(user: testUser1) .tabItem { Image(systemName: "person") Text("Account") }.tag(2) // AccountView } // TabView .accentColor(.white) } else if (authSession.currentUser == nil) { AuthView() } }// Group .onAppear(perform: authenticationListener) } // MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++ func authenticationListener() { // Setup Authentication Listener authSession.listen() } } 

Any help would be greatly appreciated.

2
  • could you show us (the code) where and how you call "MarketplaceView" starting from "ContentView" Commented Jul 13, 2021 at 23:49
  • @workingdog Happy to. Please see the edits above. Commented Jul 14, 2021 at 0:02

2 Answers 2

1

in your app you have:

ContentView().environmentObject(marketplaceViewModel) 

so in "ContentView" you should have as the first line:

@EnvironmentObject var marketplaceViewModel: MarketplaceViewModel 

Note in "ContentView" you have, "@EnvironmentObject var authSession: AuthSession" but this is not passed in from your App.

Edit: test passing "marketplaceViewModel", using this limited setup.

class MarketplaceViewModel: ObservableObject { ... let showMiki = "here is Miki Mouse" ... } 

and

struct MarketplaceView: View { @EnvironmentObject var marketplaceViewModel: MarketplaceViewModel var body: some View { // ERROR NOT HERE Text(marketplaceViewModel.showMiki) // Text(self.marketplaceViewModel.listingRowViewModels[1].listing.title) } } 
Sign up to request clarification or add additional context in comments.

6 Comments

I tried this and it did not solve the problem.
I updated the code to show my changes and added AuthSession.
try to put "@EnvironmentObject var marketplaceViewModel: MarketplaceViewModel" before "@EnvironmentObject var authSession: AuthSession" in your ContentView.
updated the answer with a simple test (that works for me) of passing the EnvironmentObject.
I’ll jump on this in the morning and start from scratch. I’ll report back.
|
1

Anyone looking for a way to use MVVM with Firebase Firestore and make your View Model the EnvironmentObject I've added my code below. This project has a list view and a detail view. Each view has a corresponding view model. The project also uses a repository and uses Combine.

App.swift

import SwiftUI import Firebase @main struct MVVMTestApp: App { @StateObject private var marketplaceViewModel = MarketplaceViewModel(listingRepository: ListingRepository()) // Firebase init() { FirebaseApp.configure() } var body: some Scene { WindowGroup { ContentView() .environmentObject(marketplaceViewModel) } } } 

ContentView.swift

import SwiftUI struct ContentView: View { var body: some View { Group { MarketplaceView() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } 

MarketplaceView.swift

import SwiftUI struct MarketplaceView: View { @EnvironmentObject var marketplaceViewModel: MarketplaceViewModel var body: some View { NavigationView { List { ForEach(self.marketplaceViewModel.listingRowViewModels, id: \.id) { listingRowViewModel in NavigationLink(destination: ListingDetailView(listingDetailViewModel: ListingDetailViewModel(listing: listingRowViewModel.listing))) { ListingRowView(listingRowViewModel: listingRowViewModel) } } // ForEach } // List .navigationTitle("Marketplace") } // NavigationView } } struct MarketplaceView_Previews: PreviewProvider { static var previews: some View { MarketplaceView() } } 

ListingRowView.swift

import SwiftUI struct ListingRowView: View { @ObservedObject var listingRowViewModel: ListingRowViewModel var body: some View { VStack(alignment: .leading, spacing: 5) { Text(listingRowViewModel.listing.name) .font(.headline) Text(String(listingRowViewModel.listing.description)) .font(.footnote) } } } struct ListingRowView_Previews: PreviewProvider { static let listingRowViewModel = ListingRowViewModel(listing: testListing1) static var previews: some View { ListingRowView(listingRowViewModel: listingRowViewModel) } } 

ListingDetailView.swift

import SwiftUI struct ListingDetailView: View { var listingDetailViewModel: ListingDetailViewModel var body: some View { VStack(spacing: 5) { Text(listingDetailViewModel.listing.name) .font(.headline) Text(String(listingDetailViewModel.listing.description)) .font(.footnote) } } } struct ListingDetailView_Previews: PreviewProvider { static let listingDetailViewModel = ListingDetailViewModel(listing: testListing1) static var previews: some View { ListingDetailView(listingDetailViewModel: listingDetailViewModel) } } 

MarketplaceViewModel.swift

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

ListingRowViewModel.swift

import Foundation import SwiftUI import Combine class ListingRowViewModel: ObservableObject { var id: String = "" @Published var listing: Listing private var cancellables = Set<AnyCancellable>() init(listing: Listing) { self.listing = listing $listing .receive(on: RunLoop.main) .compactMap { listing in listing.id } .assign(to: \.id, on: self) .store(in: &cancellables) } } 

ListingDetailViewModel.swift

import Foundation import SwiftUI import Combine class ListingDetailViewModel: ObservableObject, Identifiable { var listing: Listing init(listing: Listing) { self.listing = listing } } 

Listing.swift

import Foundation import SwiftUI import FirebaseFirestore import FirebaseFirestoreSwift struct Listing: Codable, Identifiable { @DocumentID var id: String? var name: String var description: String } 

ListingRepository.swift

import Foundation import Firebase import FirebaseFirestore import FirebaseFirestoreSwift class ListingRepository: ObservableObject { // MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++ // Access to Firestore Database let db = Firestore.firestore() @Published var listings = [Listing]() init() { startSnapshotListener() } // MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++ func startSnapshotListener() { db.collection("listings").addSnapshotListener { (querySnapshot, error) in if let error = error { print("Error getting documents: \(error)") } else { guard let documents = querySnapshot?.documents else { print("No Listings.") return } self.listings = documents.compactMap { listing in do { return try listing.data(as: Listing.self) } catch { print(error) } return nil } } } } } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.