No ObservableObject of type <TypeName> found. A View.environmentObject(_:) for <TypeName> may be missing as an ancestor of this view

Viewed 1539

I have three views (AddMatchView, TeamPickerView and TeamsOfCountryView). Everything should work like this: from AddTeamView I go to TeamPickerView, there I select the country and go to TeamsOfCountryView, after tapping on the team I need, both views (TeamPickerView and TeamsOfCountryView) should immediately close thanks to the common Bool variable, and the selected team (an object of the Team type) passed to the parent AddMatchView. But after selecting a team, only TeamsOfCountryView is closed, and an error occurs in TeamPickerView: Fatal error: No ObservableObject of type DBService found. A View.environmentObject (_ :) for DBService may be missing as an ancestor of this view.

Everything worked as it should until I decided to create a ViewModel for my view. i.e. if I transfer @State variables from AddMatchView to TeamPickerView, then errors will not stink, but I use properties from @ObservedObject

Main parent view:

struct AddMatchView: View {
@ObservedObject var viewModel = AddMatchViewModel()

@State isPresented = false //This works!
@State team: Team? //This works!

var body: some View {
    //...
    Button(action: { viewModel.isPresented.toggle() }){
        //...
    }.fullScreenCover(isPresented: $viewModel.isPresented) { TeamPickerView(team: $viewModel.home, isPresented: $viewModel.isPresented)}
//.fullScreenCover(isPresented: $isPresented) { TeamPickerView(team: $home, isPresented: $isPresented)} THIS WORKS!!
//...
}

ViewModel after using of which the error began to occur:

class AddMatchViewModel: ObservableObject{
@Published var home: Team?
@Published var isPresented = false
//...
}

TeamsPickerView:

struct TeamPickerView: View {
    @EnvironmentObject var db: DBService 
    
    @Binding var team: Team?
    
    @Binding var isPresented: Bool

    @State private var searchText = ""
    
    var body: some View {
        NavigationView {
            VStack{
                SearchBar(text: $searchText)
                Form{
                    //ERROR in below line after selecting team in child TeamsOfCountryView: No ObservableObject of type DBService found. A View.environmentObject(_:) for DBService may be missing as an ancestor of this view.
                    List (db.countries.filter({ searchText.isEmpty ? true : $0.name.contains(searchText) })) { country in
                        NavigationLink(destination: TeamsOfCountryView(countryID: country.documentID, team: $team, isPresented: $isPresented)) {
                            HStack{
                                Image(uiImage: Flag(countryCode: country.code)!.image(style: .roundedRect)).resizable().scaledToFit().frame(maxWidth: 30, maxHeight: 30)
                                Text(country.name)
                            }
                        }
                    }
                }
                
            }
            .navigationBarTitle(Text("Countries"))
            .navigationBarItems(trailing: Button(action: {isPresented = false }){
                Text("Close")
            })
        }
    }
}

TeamsOfCountryView:

struct TeamsOfCountryView: View {
    @EnvironmentObject var db: DBService
    
    @Binding var team: Team?

    @Binding var isPresented: Bool

    //...

    var body: some View {
        VStack{
            //...
            Form{
                List (teams.filter({ searchText.isEmpty ? true : $0.name.contains(searchText) })) { team in
                    Button(action: {
                        //After that, an error occurs in the parent TeamPickerView
                        self.team = team
                        self.isPresented = false
                    }){
                        //...
                    }
                }
            }
            //...
        }
    }

LITTLE UPDATE: simplified the example as much as possible

class ViewModel: ObservableObject{
    @Published var isPresented = false
}

class EnvObj: ObservableObject{
    @Published var foo = "test"
}

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()

    //@State var isPresented = false   no error if we use it instead of viewModel.isPresented
    
    var body: some View {
        Button("Open Child View A"){
            viewModel.isPresented.toggle()
        }.fullScreenCover(isPresented: $viewModel.isPresented){ChildViewA(isPresented: $viewModel.isPresented)}
    }
}

struct ChildViewA: View {
    @EnvironmentObject var envObj: EnvObj
    @Binding var isPresented: Bool
    
    @State var openChildB = false

    var body: some View {
        NavigationView{
            VStack{
            //ERROR HERE: No ObservableObject of type EnvObj found. A View.environmentObject(_:) for EnvObj may be missing as an ancestor of this view.
            Text(envObj.foo)
            NavigationLink(destination: ChildViewB(isPresented: $isPresented)){
                Text("Open Child View B")
                
            }
            }
        }
    }
}

struct ChildViewB: View {
    @EnvironmentObject var envObj: EnvObj
    @Binding var isPresented: Bool
    
    var body: some View {
        Button("Close Child View A and B"){
            isPresented = false
        }
    }
}
2 Answers

You need to inject the @EnvironmentObject into each environment (remember that each .sheet or .fullScreenCover creates a new environment):

Button("Open Child View A"){
    viewModel.isPresented.toggle()
}
.fullScreenCover(isPresented: $viewModel.isPresented) {
    ChildViewA(isPresented: $viewModel.isPresented)
        .environmentObject(EnvObj()) // inject here
}

Note: the EnvironmentObject must be created first. You can't access it by @EnvironmentObject var envObj: EnvObj if it isn't created and injected in the first place.

Also, you don't really need to create your dependencies directly in the fullScreenCover closure. You can put them at the root level and inject accordingly.

My best guess is that you need a strong reference to your DBService object when you pass it in to the ContentView. In the SceneDelegate (or App object) where you declare your content view, instead of ContentView(DBService()) do this:

let dbService = DBService() // <- strong reference, stored property at class/struct level

func scene() {
    ContentView(dbService) // <- this is the stored instance, not a brand new DBService
}

// (or)

var body: some Scene {
    ContentView(dbService) // <- this is the stored instance, not a brand new DBService
}

Related