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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.