The translation of the article was prepared on the eve of the launch of the advanced course "iOS Developer" .
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 after 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 out how to navigate between different views using 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: use @EnvironmentObject
!Hint: You can download the latest developments here (this is the “NavigateInSwiftUIComplete” folder) : GitHubWhy use ObservableObject
is 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 in the end can be 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 just the job EnvironmentObject
!What is EnvironmentObject
?
EnvironmentObject
Is a data model that, after initialization, can exchange data with all representations of your application. What is especially good is what is EnvironmentObject
created by providing ObservableObject
, so we can use ours ViewRouter
to create EnvironmentObject
!As soon as we declared ours ViewRouter
as EnvironmentObject
, all views can be attached to it as well as to normal ones ObservableObject
, but without the need for a chain of initializations down the application hierarchy!As already mentioned, it EnvironmentObject
should already be initialized the first time it is accessed. Since ours MotherView
, as the root view, will look at the property CurrentPage
ViewRouter
, we must initialize it EnvironmentObject
when the application starts. Then we can automatically change currentPageEnvironmentObject
of ContentView
, which then calls MotherView
for re-rendering.
Implementation ViewRouter
asEnvironmentObject
So, let's update the code of our application!First, change the wrapper of the property viewRouter
inside MotherView
from @ObservableObject
to @EnvironmentObject
.import SwiftUI
struct MotherView : View {
@EnvironmentObject var viewRouter: ViewRouter
var body: some View {
}
}
The property is viewRouter
now looking ViewRouter-EnvironmentObject
. Thus, we need to provide our structure MotherView_Previews
with the appropriate instance:#if DEBUG
struct MotherView_Previews : PreviewProvider {
static var previews: some View {
MotherView().environmentObject(ViewRouter())
}
}
#endif
As mentioned above - when starting our application, it should be immediately provided with an instance ViewRouter
in quality EnvironmentObject
, since it MotherView
now refers to as the root representation EnvironmentObject
. Therefore, update the scene function inside the file SceneDelegage.swift
as follows: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
in quality EnvironmentObject
, to which all views of our application can now be attached.Next, let's update ours ContentViewA
. Change its viewRouter
property to 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
is associated with the instance created when the application was launched!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 viewRouter
our properties are ContentView
now directly related to / observe the initial instance ViewRouter
as EnvironmentObject
, we no longer need to initialize them in ours 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 ours MotherView
and pass its instance down to the 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 the willChange
method that we call the file ViewRouter.swift
when CurrentPage
updated. As you already know, this causes a binding MotherView
to re-display your body, ultimately showing another ContentView
, which means moving to another ContentView
. We can add animation by simply wrapping the method willChange
in a 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 animation provided”
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 the transition modifier when called 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 a 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 all the 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”