1

I am working with SwiftUI and am using MVVM with my VM acting as my EnvironmentObjects.

I first create a AuthSession environment object which has a string for currentUserId stored.

I then create another environment object for Offers that is trying to include the AuthSession environment object so I can can filter results pulled from a database with Combine. When I add an @EnvironmentObject property to the Offer view model I get an error stating that AuthSession is not passed. This makes sense since it's not a view.

My question is, is it best to sort the results in the view or is there a way to add an EnvironmentObject to another EnvironmentObject? I know there is an answer here, but this model answer is not using VM as the EOs.

App File

@main
struct The_ExchangeApp: App {
    
    // @EnvironmentObjects
    @StateObject private var authListener = AuthSession()
    @StateObject private var offerHistoryViewModel = OfferHistoryViewModel(offerRepository: OfferRepository())
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(authListener)
                .environmentObject(offerHistoryViewModel)
        }
    }
}

AuthSession.swift

class AuthSession: ObservableObject {
    @Published var currentUser: User?
    @Published var loggedIn = false
    @Published var currentUserUid = ""
    
    // Intitalizer
    init() {
        
        self.getCurrentUserUid()
    }
}

OfferHistoryViewModel.swift - The error is called just after the .filter in startCombine().

class OfferHistoryViewModel: ObservableObject {
    // MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++
    
    // Access to AuthSession for filtering offer made by the current user.
    @EnvironmentObject var authSession: AuthSession

    // Properties
    var offerRepository: OfferRepository
    
    // Published Properties
    @Published var offerRowViewModels = [OfferRowViewModel]()
    
    // Combine Cancellable
    private var cancellables = Set<AnyCancellable>()
    
    // MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++
    
    // Intitalizer
    init(offerRepository: OfferRepository) {
        self.offerRepository = offerRepository
        self.startCombine()
    }
    
    // Starting Combine - Filter results for offers created by the current user only.
    func startCombine() {
        offerRepository
            .$offers
            .receive(on: RunLoop.main)
            .map { offers in
                offers
                    .filter { offer in
                        (self.authSession.currentUserUid != "" ? offer.userId == self.authSession.currentUserUid : false) // ERROR IS CALLED HERE
                    }
                    .map { offer in
                        OfferRowViewModel(offer: offer)
                    }
            }
            .assign(to: \.offerRowViewModels, on: self)
            .store(in: &cancellables)
    }
}

Error

Thread 1: Fatal error: No ObservableObject of type AuthSession found. A View.environmentObject(_:) for AuthSession may be missing as an ancestor of this view.
jonthornham
  • 2,881
  • 4
  • 19
  • 35

1 Answers1

0

I solved this by passing currentUserUid from AuthSession from my view to the view model. The view model changes to the following.

class OfferHistoryViewModel: ObservableObject {
    // MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++
    
    var offerRepository: OfferRepository
    
    // Published Properties
    @Published var offerRowViewModels = [OfferRowViewModel]()
    
    // Combine Cancellable
    private var cancellables = Set<AnyCancellable>()
    
    // MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++
    
    // Intitalizer
    init(offerRepository: OfferRepository) {
        self.offerRepository = offerRepository
    }
    
    // Starting Combine - Filter results for offers created by the current user only.
    func startCombine(currentUserUid: String) {
        offerRepository
            .$offers
            .receive(on: RunLoop.main)
            .map { offers in
                offers
                    .filter { offer in
                        (currentUserUid != "" ? offer.userId == currentUserUid : false)
                    }
                    .map { offer in
                        OfferRowViewModel(offer: offer)
                    }
            }
            .assign(to: \.offerRowViewModels, on: self)
            .store(in: &cancellables)
    }
}

Then in the view I pass the currentUserUid in onAppear.

struct OfferHistoryView: View {
    // MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++
        
    @EnvironmentObject var authSession: AuthSession
    @EnvironmentObject var offerHistoryViewModel: OfferHistoryViewModel
    
    // MARK: ++++++++++++++++++++++++++++++++++++++ View ++++++++++++++++++++++++++++++++++++++
    
    var body: some View {
         // BuildView
        } // View
        .onAppear(perform: {
            self.offerHistoryViewModel.startCombine(currentUserUid: self.authSession.currentUserUid)
        })
    }
}

This works well for me and I hope it helps someone else.

jonthornham
  • 2,881
  • 4
  • 19
  • 35