Reputation: 3335
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
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
Reputation: 36314
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