How to navigate between views in SwiftUI by using an @EnvironmentObject

Share on facebook
Share on twitter
Share on pinterest
Updated for Xcode 12 and SwiftUI 2.0 ✅

Hello and welcome to this tutorial! In this post, we will talk about how to navigate between views in SwiftUI (not using a navigation view!). A concept that may sounds trivial but by understanding it deeply we can learn a lot about the data flow concepts used in SwiftUI.

This tutorial is an excerpt from our Interactive Mastering SwiftUI Book

In the last part, we learned how to do this by using an ObservableObject and the @StateObject property wrapper. In this part, we look at how to accomplish the same but more efficiently using an @EnvironmentObject. We are also going to apply a nice transition animation.

Here is what we are going to achieve:

Our current progress 👓📚

So we just figured out how to navigate between different views using a ObservableObject. In a nutshell, we created a ViewRouter and bound our MotherView and the ContentViews to it. Then, we update the Page assigned to the ViewRouter’s currentPage property when clicking on the respective ContentViews Buttons. This causes the MotherView to update its body with eventually showing the correct ContentView!

But there is a second, more efficient way for achieving this functionality: using an @EnvironmentObject!

Hint: You can download the current progress here (it’s the “NavigatinInSwiftUIFinishedPart1” folder):

GitHub

Why using an ObservableObject isn’t always the best solution⚠

Okay, we just learned how ObservableObjects and SwiftUI work and how we can utilize them for navigating between views.

Maybe you’re asking yourself: “Why should we do anything else when the existing solution is sufficient?”. Well, it should become clear when looking at our app’s hierarchy logic. The NavigatingInSwiftUIApp struct initialises a ViewRouter instance as a @StateObject and passes it to the root MotherView. In the MotherView, we initialise either ContentViewA or ContentViewB with passing down the ViewRouter instance down to them. 

You see that we follow a strict hierarchy which passes the initialised ViewRouter as a @StateObject downwards to the “lowest” subviews. For our purposes, this not a big deal, but imagine a more complex app with a lot of views containing subviews that in turn contain subviews and so on. Passing down the primary initialised @StateObject down to all subviews could get pretty messy.

In one sentence: Using an ObservableObjects observed by using @StateObjects can become confusing when we work with more complex app hierarchies.

So, what we could do instead, is to initialise the ViewRouter once at the app’s launch in a way that all views of our app hierarchy can be directly bound to this instance, or better said, are observing this instance, with no regard to the app’s hierarchy and no need to passing the ViewRouter downwards the hierarchy manually. The ViewRouter instance would then act like a cloud that flies above our app’s code where all views have access to, without taking care of a proper initialisation chain downwards the app’s view hierarchy.

Doing this is the perfect job for an EnvironmentObject!

What is an EnvironmentObject? 🧐

An EnvironmentObject is a data model that, once initialised, can be used to share information across all view’s of your app. The cool thing is, that an EnvironmentObject is created by supplying an ObservableObject. Thus we can use our ViewRouter as it its for creating an EnvironmentObject!

So, once we defined our ViewRouter as an EnvironmentObject, all views can be bound to it in the same way as a regular ObservableObject but without the need of an initialisation chain downwards the app’s hierarchy.

As said, an EnvironmentObject needs to already be initialised when referring to it the first time. Since our root MotherView will look into the ViewRouter‘s currentPage property first, we need to initialise the EnvironmentObject at the app’s launch. We can then automatically change the data assigned to the EnvironmentObject’s currentPage property from the ContentView’s which then causes the MotherView to rerender its body.

Implementing the ViewRouter as an EnvironmentObject 🤓

Let’s update our app’s code!

First, change the viewRouter‘s property wrapper inside the MotherView from an @StateObject to an @EnvironmentObject.

@EnvironmentObject var viewRouter: ViewRouter

Now, the viewRouter property looks for a ViewRouter as an EnvironmentObject instance. Thus, we need to provide our MotherView_Previews struct with such an instance:

struct MotherView_Previews: PreviewProvider {
    static var previews: some View {
        MotherView().environmentObject(ViewRouter())
    }
}

When launching our app, the first and most high view in the app hiearchy must immediately be provided with a ViewRouter instance as an EnvironmentObject. Therefore, we need to pass the @StateObject we initialised in our NavigatingInSwiftUIApp struct to the MotherView as an injected EnvironmentObject like this.

@main
struct NavigatingInSwiftUIApp: App {
    
    @StateObject var viewRouter = ViewRouter()
    
    var body: some Scene {
        WindowGroup {
            MotherView().environmentObject(viewRouter)
        }
    }
}

Great! SwiftUI now creates a ViewRouter instance and injects it to the whole view hierarchy as an EnvironmentObject when the app launches. Now, all views of our app can bound to this EnvironmentObject. 

Next, let’s update our ContentViewA. Change the viewRouter property to an EnvironmentObject …

@EnvironmentObject var viewRouter: ViewRouter

… and update the ContentViewA_Previews struct:

struct ContentViewA_Previews: PreviewProvider {
    static var previews: some View {
        ContentViewA().environmentObject(ViewRouter())
    }
}

Hint: Again, only the ContentViewsA_Previews struct has an own instance of the ViewRouter, but the ContentViewA itself is bound to the instance created at the app’s launch!

Let’s repeat this for ContentViewB…

@EnvironmentObject var viewRouter: ViewRouter

…and its previews struct:

struct ContentViewB_Previews: PreviewProvider {
    static var previews: some View {
        ContentViewB().environmentObject(ViewRouter())
    }
}

Since the viewRouter properties of our ContentViews are now directly bound to the initial ViewRouter instance as an EnvironmentObject, we don’t need to initialise them inside our MotherView anymore. So, let’s update our MotherView:

struct MotherView: View {
    
    @EnvironmentObject var viewRouter: ViewRouter

    var body: some View {
        switch viewRouter.currentPage {
            case .page1:
                ContentViewA()
            case .page2:
                ContentViewB()
        }
    }
}

And that’s the cool thing about EnvironmentObjects. We don’t need to pass down the viewRouter of our MotherView downwards to ContentView’s anymore. This can be very efficient, especially for more complex hierarchies.

Great! Let’s run our app and see if that works … perfect, we are still able to navigate between our different views but with a more clean code.

Adding a transition animation 🚀

Before ending this tutorial, let’s take a look at how to add a transition animation when navigating from .page1 to .page2.

Doing this in SwiftUI is pretty straight forward.

Take a look at the ViewRouter’s currentPage property that we manipulate when we tap on the Next/Back Button. As you learned, due to the @Published property wrapper’s functionality, this triggers the bound MotherView to rerender its body with eventually showing another ContentView. We can simply animate this navigation process by wrapping the code that changes the Page assigned to the currentPage into a “withAnimation” statement. Let’s do this for the Button of ContentViewA

Button(action: {
    withAnimation {
        viewRouter.currentPage = .page2
    }
}) {
    NextButtonContent()
}

… and ContentViewB

Button(action: {
    withAnimation {
        viewRouter.currentPage = .page1
    }
}) {
    BackButtonContent()
}

Now, we present a transition animation when navigating to another ContentView. 

“withAnimation(_:_:) – Returns the result of recomputing the view’s body with the provided animation”

Apple

By default, the “withAnimation” statement uses a fade transition style. But instead, we want to show a “pop up” transition when navigating from ContentViewA to ContentViewB. To do this, go into your MotherView.swift file and add a transition modifier when calling the ContentViewB. You can choose between several preset transition styles or create even a custom one (but that’s a topic for another tutorial). For adding a “pop up” transition, we choose the .scale transition type.

switch viewRouter.currentPage {
case .page1:
    ContentViewA()
case .page2:
    ContentViewB()
        .transition(.scale)
}

Hint: Most animations don’t work within the preview simulator. Try running your app in the regular simulator instead.

Awesome! With just a few lines of code, we added a nice transition animation to our app.

You can download the whole source code here!

Conclusion 🎊

That’s it! We learned when and how to use EnvironmentObjects in SwiftUI. We also learned how to add a transition animation to view showing up.

You are now capable of navigating between views in SwiftUI by using two ways – either you put your views into a navigation view hierarchy or you create an external view router as an Observable-/EnvironmentObject.

You liked the tutorial and you want to learn more about developing iOS apps with SwiftUI? Then check out our Interactive Mastering SwiftUI Book!

20 replies on “How to navigate between views in SwiftUI by using an @EnvironmentObject”

I have tried both your examples, ObservedObject & EnvironmentObject. Both operate correctly from Grumpy to Happy. Both fail to go back showing instead a blank white screen. If I download you project file from github it works correctly in both directions. From researching I learn there is a bug in NavigationLink in iOS 13.3. Is this my issue, I would welcome any comment.

Hey Richard,

please send us your code so we can look through it & help you! Just upload your Project to GitHub and send us the link.

I needed to go from a login screen to a master-detail screen, and this was the best way that I found to do it, thanks.

Why “mother” view? Its pretty much standard to call that type of thing parent view. Should probably try sticking to standard terminology as to not confuse beginners – a big part of learning is knowing which keywords to google.

I’ve been through several tutorials on EnvironmentObject etc. and this one finally made me understand how to get my SwiftUI code to work with it. Many thanks!

Hello,

Stay healthy, stay home…

Thank you for this clear and concise tutorial.
Loved it !
I followed along using the [kinda]new @Published declaration of my variable in the ViewRouter class so as to simplify it to the minimalist :

class ViewRouter: ObservableObject
{ @Published var currentPage = “page1”}

Works like a charm until I try to animate the views !

Any idea how to implement the animation without reverting to the heavier code :

var currentPage: String = “page1” {
didSet {
withAnimation() {
willChange.send(self)
}
}
}

Best regards

Hey Serge, thanks for your comment. That’s the same problem I dealt with. This I why I decided to stick with the current approach ..

I researched for 3 hours on how to make this work, multiple articles. this one solved everything I needed! Amazing work and thanks! this is really good! the explanation fo what is happening is great

So, to keep with conventions, I have used buttons in the Navigation Bar to handle the “go back” without doing anything movement in the navigation hierarchy. However, I can’t make the last button disappear in the MotherView. Any thoughts?

For anyone who’s having issues with the animations, I found out sometimes they don’t work in the live preview; only when running in the simulator.

Great tutorial! Save me tons of time!

I did just run into one edit change though above when you say:

So let’s update our app’s code!

First, change the viewRouter property wrapper inside the MotherView from an @ObservableObject to an @EnvironmentObject.

import SwiftUI

struct MotherView : View {
@EnvironmentObject var viewRouter: ViewRouter
var body: some View {
//…
}
}

—-
In the MotherView, the previous reference was to an @ObservedObject and not a @ObservableObject.

My understanding is that when BindableObject became ObservableObject, the PassthroughSubject went from didChange to willChange, which should now be called in the willSet handler. Alas, I have code that seems only to work right when I use a didSet to call willChange, as you did. What is the sequence of state-updating events?

Thank you for this excellent tutorial. This scratched an itch and now I’m much happier with my implementation. I also, as others mentioned, had to restore animation since the @Published API changed. Here’s what I like for restoring animation.

public class Router: ObservableObject {
@Published var activeView = “first”
}

extension AnyTransition {
static var inOutLeading: AnyTransition {
AnyTransition.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
}

static var inOutTrailing: AnyTransition {
AnyTransition.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
}
}

struct RouterView: View {
@EnvironmentObject private var router: Router
@State var activeView = “”

var body: some View {
VStack {
if activeView == “first” {
Text(“first”)
.transition(.inOutLeading)
} else if activeView == “second” {
Text(“second”)
.transition(.inOutTrailing)
}
}
.animation(.easeIn(duration: 0.7))
.onReceive(router.$activeView, perform: { activeView in
withAnimation {
self.activeView = activeView
}
})
}
}

Then add a view that sets Router.activeView as needed.

why did you use this code : ” let objectWillChange = PassthroughSubject()

var currentPage: String = “page1″ {
didSet{
objectWillChange.send(self)
}
} ” inside the ViewRouter class? Couldn’t you just have used @Published wrapper to achieve the same thing? I used @Published and it worked. Is there any downside to using it?
This is how my ViewRouter class looks like
´´´ class ViewRouter: ObservableObject {
@Published var currentPage: String = “page1”
} ´´´

Yes, you can use this property as well. The only downside is that you can animate the change using the withAnimation {…} wrapper without relying on a didSet clause (:

This was super helpful! I was struggling, ready to give up on SwiftUI, when I found this tutorial. As usual, Apple makes it very easy to do some things, at the expense of doing others. Now I still lose some screen real estate, and control over the design, when I use NavigationView within your approach, but oh well, as I said, some things are easy with SwiftUI. Thanks!

Leave a Reply

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

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount