Navigating between views using @EnvironmentObject in SwiftUI

The translation of the article was prepared in advance of the start 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 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 ObservableObjectso 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 ViewRouterwe must initialize EnvironmentObject when you start the application. Then we can automatically change currentPage EnvironmentObject of ContentViewwhich 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 EnvironmentObjectto 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 ViewRouterbut 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 ContentViewwhich 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”


Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *