Manny M.
Manny M.

Reputation: 49

How do I instantiate a view with multiple EnvironmentObjects?

I was learning about how EnvironmentObject works for a school project, and I was confused about how to instantiate a view with multiple EnvironmentObjects. For example, the following code:

import SwiftUI

class names: ObservableObject {
    @Published var myName = ""
}

struct FirstView: View {
    @StateObject var FirstName = names()
    
    var body: some View {
        NavigationView {
            VStack {
                TextField("Type", text: $FirstName.myName)
                NavigationLink(destination: SecondView()) {
                    Text("Second View")
                }
            }
        }.environmentObject(FirstName)
    }
}

struct SecondView: View {
    @StateObject var LastName = names()
    var body: some View {
        VStack {
            TextField("Type", text: $LastName.myName)
            NavigationLink(destination: ThirdView().environmentObject(FirstName).environmentObject(LastName)) {
                Text("Third View")
            }
        }.environmentObject(LastName)
    }
}

struct ThirdView: View {
    @EnvironmentObject var FirstName: names
    @EnvironmentObject var LastName: names
    var body: some View {
        Text("Full name: \(FirstName.myName) \(LastName.myName)")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        FirstView()
    }
}

I need ThirdView to receive FirstName from FirstView and LastName from SecondView, but I can't instantiate ThirdView from SecondView with the required Environment Objects; this code above crashes with the error "Cannot find FirstName in scope".

Alternatively, If I try to instantiate ThirdView with only LastName as an environment object, the code will present something like "Smith Smith" if I entered "John" in the text field on FirstView and "Smith" in the text field on SecondView.

Can someone tell me what I'm doing wrong? Thank you! :)

Upvotes: 0

Views: 227

Answers (3)

Damiaan Dufaux
Damiaan Dufaux

Reputation: 4785

You are probably looking for EnvironmentKeys. Use them like this:

private struct FirstNameKey: EnvironmentKey {
    static let defaultValue = "No first name"
}
private struct LastNameKey: EnvironmentKey {
    static let defaultValue = "No last name"
}

And add them to your EnvironmentValues:

extension EnvironmentValues {
    var firstName: String {
        get { self[FirstNameKey.self] }
        set { self[FirstNameKey.self] = newValue }
    }
    var lastName: String {
        get { self[LastNameKey.self] }
        set { self[LastNameKey.self] = newValue }
    }
}

They values can then be bound to the environment like this:

var body: some View {
    MyCustomView()
        .environment(\.firstName, "John")
        .environment(\.lastName, "Doe")
}

And retrieved like this:

struct ThirdView: View {
    @Environment(\.firstName) var firstName
    @Environment(\.lastName) var lastName
    var body: some View {
        Text("Full name: \(firstName) \(lastName)")
    }
}

Side note on conventions

To understand code more easily the Swift.org community asks to

Give types UpperCamelCase names (such as SomeStructure and SomeClass here) to match the capitalization of standard Swift types (such as String, Int, and Bool). Give properties and methods lowerCamelCase names (such as frameRate and incrementCount) to differentiate them from type names.

So it would be better to write your class names as class Names as it greatly improves readability for Swift users.

Upvotes: 1

lorem ipsum
lorem ipsum

Reputation: 29242

Since they are of the same type you can’t have two. SwiftUI can’t tell the difference

//Names for classes and structs should start with an uppercase letter
class PersonModel: ObservableObject {
    @Published var firstName = ""
    @Published var lastName = ""
}

struct FirstNameView: View {
    //variables start with lowercase
    @StateObject var person: PersonModel = PersonModel()
    
    var body: some View {
        NavigationView {
            VStack {
                TextField("Type", text: $person.firstName)
                NavigationLink(destination: LastNameView()) {
                    Text("Second View")
                }
            }
        }.environmentObject(person)
    }
}

struct LastNameView: View {
    @EnvironmentObject var person: PersonModel
    var body: some View {
        VStack {
            TextField("Type", text: $person.lastName)
            NavigationLink(destination: FullNameView()) {
                Text("Third View")
            }
        }
    }
}

struct FullNameView: View {
    @EnvironmentObject var person: PersonModel
    var body: some View {
        Text("Full name: \(person.firstName) \(person.lastName)")
    }
}

Upvotes: 2

AbdelAli
AbdelAli

Reputation: 816

environmentObject(_:) modifier method takes an ObservableObject and passes it down the view tree. It works without specifying an environment key because the type of the object is automatically used as the key.

So to resume your last name instance is somehow invalidating your first name instance.

I'd then suggest either to create a model that contains both first and last name or simply use @Environment with a key (as it's suggested by Damiaan Dufaux) if it’s possible to get away with passing a value type, because it’s the safer mechanism.

Upvotes: 1

Related Questions