You have a view that is so deep. This is because you want to split the children's view for easier work with them. A nested view requires you to share model data to its children so they are automatically updated when the data changes.
At first, you may solve the sharing data problem by using the view initializer. Doing so with a lot of views starts to be cumbersome for you. Passing from A to B to C can grow exponentially and can hurt your eyes just by looking at the source code. It also makes maintenance so painful. Suppose the view that uses the data is located 5 views deep, then you must pass the model several times before using it.
Using
@EnvironmentObject
can solve the above issue. @EnvironmentObject
helps you automagically get a reference to the model data and listen for change. Whenever data is updated, your view can react to the new data automatically.How to define model data
Defining model data is easy. You just need to create a class that conforms to
ObservableObject
.class UserViewModel: ObservableObject { @Published var name: String = "" @Published var age: Int = 0 }
Your model data must conform to
ObservableObject
in order to use @EnvironmentObject
The above
UserViewModel
defines two things:name
is a published property of typeString
age
is a published property of typeInt
Updating those values means the view that reads them must also update.
How to send data to children
Sending data to children can be done using
.environmentObject
modifier. This modifier allows you to pass an object that conforms to the ObservableObject
protocol.Here is the code to use
UserViewModel
in ContentView
struct UserView: View { @EnvironmentObject var user: UserViewModel var body:some View { Text("User: \(user.name)") AgeView() } } struct AgeView: View { @EnvironmentObject var user: UserViewModel var body: some View { Text("Age: \(user.age)") } } struct ContentView: View { @StateObject private var user = UserViewModel() var body:some View { NavigationStack { VStack { UserView() Button("Update Name") { user.name = "Mozzlog" } Button("Update Age") { user.age += 1 } } } .environmentObject(settings) } }
You must provide ancestor view
.environmentObject
before read it using @EnvironmentObject
. This also applies to the SwiftUI Preview. If you don’t do that, it can cause a crash.The explanation of the above code is as follows:
- You have
UserView
which is responsible for displaying text showing the user name. It reads the user value from environment hence you write@EnvironmentObject var user: UserViewModel
.
- You also have
AgeView
which is responsible for displaying text showing the user's age. It reads the user's age from the environment.
UserView
andAgeView
don’t create aUserViewModel
. They only read the value from environment, which supplied byContentView
ContentView
doesn’t need to supply theUserViewModel
object directly toUserView
because calling.environmentObject(user)
from theNavigationStack
view automatically provides that object for its children.
- Now
UserView
andAgeView
will read the user value fromContentView.user
. Any changes fromContentView
, like tapping the Update Name and Update Age buttons, will update these views.
Conclusion
To avoid passing data model multiple times, you can use
@EnvironmentObject
that will read the referenced value from its ancestor. You send the model data using the .environmentObject
modifier from the top-most ancestor. The model data will be accessible recursively to all children.