jonthornham
jonthornham

Reputation: 3335

Why Is EnvironmentObject Not Working in SwiftUI Project?

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.

Upvotes: 2

Views: 2243

Answers (2)

jonthornham
jonthornham

Reputation: 3335

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
                }
            }
        }
    }
}

Upvotes: 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)
    }
}

Upvotes: 1

Related Questions