Navigating between views using @EnvironmentObject in SwiftUI
Hello and welcome to our tutorial! In this series, we talk about how to navigate between views in SwiftUI (without using a navigation view!). Although this idea may seem trivial, but by understanding it a little deeper, we can learn a lot about the data flow concepts used in SwiftUI.
In the previous part we learned how to implement this using @ObservableObject
. In this part, we will look at how to do the same, but more efficiently using @EnvironmentObject. We are also going to add a little transition animation.
Here’s what we’re going to achieve:
What we have
So we just figured outhow to navigate between different views using an ObservableObject. In a nutshell, we created a ViewRouter and associated it with Mother View and our Content View. Next, we simply manipulate the CurrentPage ViewRouter property by clicking on the Content View buttons. After that, MotherView is updated to display the corresponding Content View!
But there is a second, more efficient way to achieve this functionality: using @EnvironmentObject
!
Hint: You can download the latest developments here (this is the “NavigateInSwiftUIComplete” folder):Github
Why use ObservableObject
– not the best solution
You are probably wondering: why should we implement this in any other way, when we already have a working solution? Well, if you look at the logic of the hierarchy of our application, then it will become clear to you. Our MotherView is the root view that initializes the ViewRouter instance. In MotherView, we also initialize ContentViewA and ContentViewB, passing them the ViewRouter instance as a BindableObject.
As you can see, we must follow a strict hierarchy that passes an initialized ObservableObject downstream to all subviews. Now this is not so important, but imagine a more complex application with a lot of views. We always need to keep track of the transmission of the initialized Observable root view to all subviews and all subviews of subviews, etc., which can ultimately become a rather tedious task.
To summarize: using clean ObservableObject
can be problematic when it comes to more complex application hierarchies.
Instead, we could initialize the ViewRouter when the application starts so that all views can be directly connected to this instance, or, rather, watch it, regardless of the hierarchy of the application. In this case, the ViewRouter instance will look like a cloud that flies over the code of our application, to which all views automatically access without worrying about the correct initialization chain down the view hierarchy.
This is a job just for EnvironmentObject
!
What EnvironmentObject
?
EnvironmentObject
Is a data model that, after initialization, can exchange data with all representations of your application. What is especially good is that EnvironmentObject
created by providing ObservableObject
so we can use our ViewRouter
for creating EnvironmentObject
!
As soon as we announced our ViewRouter
as EnvironmentObject
, all views can be attached to it in the same way as to ordinary ObservableObject
, but without the need for an initialization chain down the application hierarchy!
As already mentioned, EnvironmentObject
should already be initialized the first time it is accessed. Since our MotherView
, as the root view, will look at the property CurrentPage
ViewRouter
we must initialize EnvironmentObject
when you start the application. Then we can automatically change currentPage EnvironmentObject
of ContentView
which then calls MotherView
for re-rendering.
Implementation ViewRouter
as EnvironmentObject
So, let’s update the code of our application!
First, change the property wrapper viewRouter
inside MotherView
with @ObservableObject
on the @EnvironmentObject
.
import SwiftUI
struct MotherView : View {
@EnvironmentObject var viewRouter: ViewRouter
var body: some View {
//...
}
}
Property viewRouter
now looking ViewRouter-EnvironmentObject
. Therefore, we need to provide our structure MotherView_Previews
appropriate instance:
#if DEBUG
struct MotherView_Previews : PreviewProvider {
static var previews: some View {
MotherView().environmentObject(ViewRouter())
}
}
#endif
As mentioned above – when you run our application, it should be immediately provided with an instance ViewRouter
as EnvironmentObject
, because the MotherView
as the root view now refers to EnvironmentObject
. Therefore, update the scene function inside the file SceneDelegage.swift
in the following way:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: MotherView().environmentObject(ViewRouter()))
self.window = window
window.makeKeyAndVisible()
}
}
Great, now when you start the application, SwiftUI creates an instance ViewRouter
as EnvironmentObject
to which all representations of our application can now be attached.
Next, let’s update our ContentViewA
. Change it viewRouter
property on EnvironmentObject
, and also update the structure ContentViewA_Previews
.
import SwiftUI
struct ContentViewA : View {
@EnvironmentObject var viewRouter: ViewRouter
var body: some View {
//...
}
}
#if DEBUG
struct ContentViewA_Previews : PreviewProvider {
static var previews: some View {
ContentViewA().environmentObject(ViewRouter())
}
}
#endif
Hint: Again, the structure ContentViewsA_Previews
has its own instance ViewRouter
but ContentViewA
connected to the instance created at application launch!
Let’s repeat this for ContentViewB
:
import SwiftUI
struct ContentViewB : View {
@EnvironmentObject var viewRouter: ViewRouter
var body: some View {
//...
}
}
#if DEBUG
struct ContentViewB_Previews : PreviewProvider {
static var previews: some View {
ContentViewB().environmentObject(ViewRouter())
}
}
#endif
Since the properties viewRouter
of our ContentView
now directly linked to / watching the initial instance ViewRouter
as EnvironmentObject
, we no longer need to initialize them in our MotherView
. So let’s update our MotherView
:
struct MotherView : View {
@EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "page1" {
ContentViewA()
} else if viewRouter.currentPage == "page2" {
ContentViewB()
}
}
}
}
This is just great: we no longer need to initialize ViewRouter
inside our MotherView
and passing its instance down to ContentView, which can be very efficient, especially for more complex hierarchies.
Great, let’s run our application and see how it works.
Great, we can still move between our views!
Adding transition animations
As a bonus, let’s look at how to add a transition animation when switching from “page1” to “page2”.
In SwiftUI, this is quite simple.
Look at willChange
method that we call to file ViewRouter.swift
when CurrentPage
updated. As you already know, this causes a binding MotherView
to remapping your body, ultimately showing another ContentView
which means moving to another ContentView
. We can add animation by simply wrapping the method willChange
in function withAnimation
:
var currentPage: String = "page1" {
didSet {
withAnimation() {
willChange.send(self)
}
}
}
Now we can add a transition animation when displaying another Content View
.
“WithAnimation (_: _ 🙂 – returns the result of recalculating the view body with the provided animation”
Apple
We want to provide our application with a pop-up transition when navigating from ContentViewA
to ContentViewB
. To do this, go to the file MotherView.swift
and add transition modifier on call ContentViewB
. You can choose one of several predefined transition types or even create your own (but this is a topic for another article). To add a pop-up transition, we select the type .scale
.
var body: some View {
VStack {
if viewRouter.currentPage == "page1" {
ContentViewA()
} else if viewRouter.currentPage == "page2" {
ContentViewB()
.transition(.scale)
}
}
}
To see how it works, run your application in a regular simulator:
Cool, we added a nice transition animation to our application in just a few lines of code!
You can download the whole source code. here!
Conclusion
That’s all! We learned why it is better to use EnvironmentObject
to navigate between views in SwiftUI, and how to implement it. We also learned how to add transition animations to navigation. If you want to know more, follow us on Instagram and subscribe to our newsletter, so as not to miss updates, tutorials and tips on SwiftUI and much more!
Free lesson: “Accelerating iOS apps with instruments”