Creating a Mother View š©āš§āš¦
The first step is to create a mother view that hosts both ContentViews as its subviews. For this purpose, create a new File-New-File-SwiftUI View and name it MotherView. In this view, we want to show either ContentViewA or ContentViewB depending on where the user navigated to.Ā
Important: Since our MotherView will ācontain” the ContentViews, it must be the root view when the app launches. To set the MotherView as the root view, open the NavigatinInSwiftUIApp.swift file and replace the ContentViewA inside the WindowGroup with an instance of our MotherView.
@main
struct NavigatingInSwiftUIApp: App {
var body: some Scene {
WindowGroup {
MotherView()
}
}
}
Back to our MotherView.swift file: To keep track of the selected main view, we need to declare a State property first. By default, we want to present ContentViewA as the first “page”. For this purpose, create a new Swift file named “Helper” and insert the following enum.
enum Page {
case page1
case page2
}
Now, we can declare the State property in our MotherView and assign it to the page1 option of our Page enum.
struct MotherView: View {
@State var currentPage: Page = .page1
var body: some View {
Text("Hello World")
}
}
Depending on the Page assigned to the currentPage State, we want to either host ContentViewA or ContentViewB. Letās implement this logic by inserting a switch-statement inside a VStack.
var body: some View {
switch currentPage {
case .page1:
ContentViewA()
case .page2:
ContentViewB()
}
}
Letās run a preview and take a look at it. Since our State is currently assigned to .page1, our first switch case is met and the ContentViewA gets hosted. Letās change the page State to .page2 and see what happens.
Here we go! When our State property changes, the whole MotherView gets updated and the switch block gets executed again, this time showing us ContentViewB. So far, so good.
But we want to let the user change the currentPage State by tapping on the Buttons inside of ContentViewA and ContentViewB, respectively. Note that the Buttons are not part of the MotherView itself, so we need to create a “bridge” for accessing the MotherView’s currentPage State from the outside; meaning that when, for instance, tapping on the “Next” Button of ContentViewA, we alter the Page assigned to the currentPage State of the MotherView for eventually navigating to ContentViewB.
We can achieve this by interjecting something called an ObservableObject into our MotherView – ContentViews hierarchy.
Observable Objects ?! š¤Æ
At this point, you are probably asking yourself: “What the heck are ObservableObjects?!ā. Well, understanding this can be pretty tough but donāt worry, we will explain it to you in a simple way.
ObservableObjects are similar to State properties which you should already know. But instead of just refreshing the body of the related view when the data assigned to the State changes, ObservableObjects are capable of the following things:Ā
- Instead of variables, ObservableObjects are classes that can contain data, for example, a String assigned to a variableĀ
- We can bind multiple views to the ObservableObject (in other words: we can make these views observe the ObservableObject).
- Because an ObservableObject is just a class, the observing views can access and manipulate the data inside the ObservableObject
- When a change happens to the ObservableObjectās data, all observing views get automatically notified and refreshed similar to when the value assigned to a State changes
So, how can we utilize this functionality? Well, we can create an ObservableObject class that contains a variable indicating the current Page that should be displayed. Then we can bind our MotherView, our ContentViewA, and our ContentViewB to it. Then, we can tell our MotherView to show the corresponding ContentView depending on the Page assigned to the ObservableObject’s variable.
From the Buttons inside the ContentViews, we can update the Page assigned to the ObservableObject’s variable. This would cause all three observing views to update their bodies, including the MotherView. With this functionality, we can achieve that the MotherView will present show the correct ContentView depending on the selected Page!

Letās create such an ObservableObject. To do this, create a new Swift file and name it “ViewRouter”. Make sure you import the SwiftUI framework. Then create a class called “ViewRouter” conforming to the ObservableObject protocol.
import SwiftUI
class ViewRouter: ObservableObject {
}
As said, an ObservableObject notifies and causes all of its observing views to update themselves when a change happens. But what exactly do we mean by āwhen a change happensā? As said, the main task of our ViewRouter should be to stay tracked on which Page (meaning which ContentView) should be currently shown – whether itās on the launch of the app or when the user taps on a specific Button. For this purpose, we declare a variable called currentPage inside our ViewRouter class and assign .page1 to it as its default value.
class ViewRouter: ObservableObject {
var currentPage: Page = .page1
}
The views that will observe the ViewRouter, especially the MotherView, should get notified and updated when the Page assigned to the currentPage changes.
To do this, we use the @Published property wrapper.
@Published var currentPage: Page = .page1
The @Published property wrapper works very similarly to the @State property wrapper. Every time the value assigned to the wrapped property changes, every observing view rerenders. In our case, we want our MotherView to observe the ViewRouter and to navigate to the right Page depending on the currentPageās updated value.
Updating the MotherView š
To make the MotherView observe the ViewRouter, we need to declare a @StateObject property, which is used for binding views to ObservableObjects.
struct MotherView: View {
@State var currentPage: Page = .page1
@StateObject var viewRouter: ViewRouter
var body: some View {
//...
}
}
When doing this, we also need to provide our MotherView_Previews struct with an instance of the ViewRouter.
struct MotherView_Previews: PreviewProvider {
static var previews: some View {
MotherView(viewRouter: ViewRouter())
}
}
In the NavigatingInSwiftUIApp.swift file, we defined the MotherView as the root view when the app launches. Thus, not only does our Preview simulator need to be provided with a ViewRouter instance, but also the actual App hierarchy when the app gets executed on a real device or in the standard simulator.
Therefore, let’s go to the NavigatingInSwiftUIApp file and declare a @StateObject property. Then, pass the initialized @StateObject to the viewRouter of our MotherView.
@main
struct NavigatingInSwiftUIApp: App {
@StateObject var viewRouter = ViewRouter()
var body: some Scene {
WindowGroup {
MotherView(viewRouter: viewRouter)
}
}
}
Our MotherView router is now able to observe and access the viewRouter‘s OberservableObject. So, letās show the corresponding ContentView depending on the Page assigned to the viewRouter‘s currentPage property.
var body: some View {
switch viewRouter.currentPage {
case .page1:
ContentViewA()
case .page2:
ContentViewB()
}
}
You can delete the currentPage State of the MotherView, since we won’t need it anymore.
Letās take a look at the simulator of our MotherView: The MotherView reads the value of the ViewRouterās currentPage variable and hosts the corresponding ContentView. You can check this by changing the default value assigned to the ViewRouterās currentPage property to .page2. Go back to the MotherView Preview simulator and see what happens! The @Published property of our ObservableObject told the MotherView to update its body.
Because we want ContentViewA to be the default view, assign .page1 to the currentPage property again.
Great! We accomplished a lot so far! We initialized a ViewRouter instance and bound it to the MotherView by using a @StateObject. Every time the values assigned to the currentPage property of the ViewRouter instance gets updated, the MotherView refreshes its body with eventually showing the right ContentView!
Bind the ContentViews to the ViewRouter ā
Our MotherView is now able to show the correct ContentView depending on the Page assigned to the currentPage property of the ViewRouter. But until now, the user is not able to change this value by tapping on the respective Button of ContentViewA and ContentViewB.
Letās start with ContentViewA. To let it access the currentPage and manipulate its value we have to bind it to the ViewRouter. So, letās create an @StateObject again.
struct ContentViewA: View {
@StateObject var viewRouter: ViewRouter
var body: some View {
//...
}
}
We need to update the ContentViewA_Previews struct again.
struct ContentViewA_Previews: PreviewProvider {
static var previews: some View {
ContentViewA(viewRouter: ViewRouter())
}
}
ContentViewA should observe the ViewRouter instance we created inside the NavigatingInSwiftUI struct and passed to the MotherView. So, letās assign our new @StateObject to this instance when initializing ContentViewA in our MotherView.
switch viewRouter.currentPage {
case .page1:
ContentViewA(viewRouter: viewRouter)
case .page2:
ContentViewB()
}
Great! Now we have access to the currentPage property of our viewRouter. Use the Button’s action closure to assign .page2 to it when tapping on the “Next”-Button.
Button(action: {
viewRouter.currentPage = .page2
}) {
NextButtonContent()
}
Okay, let’s see if that works: Run the app in the regular simulator or start a Live preview of the MotherView and tap on the “Next”-Button. Great, we successfully navigate to ContentViewB!Ā
Here’s what happens when the user taps on the “Next”-Button of ContentViewA: ContentViewA changes the Page assigned to the currentPage property of the viewRouter to .page2. Therefore, the viewRouter tells all bound views to rerender their bodies, including the MotherView. The MotherView updates its body and checks the currentPage‘s value. Because itās .page2 now, the case for showing ContentViewB is met and we eventually navigate to it!
To be able to navigate back to ContentViewA, repeat this implementation process for ContentViewB:
Declare a @StateObject property as a ViewRouter instanceā¦
struct ContentViewB: View {
@StateObject var viewRouter: ViewRouter
var body: some View {
//...
}
}
ā¦and update the related previews struct:
struct ContentViewB_Previews: PreviewProvider {
static var previews: some View {
ContentViewB(viewRouter: ViewRouter())
}
}
Assign this viewRouter property to the initial ViewRouter instance passed by the NavigatingInSwiftUIApp struct to the MotherView.
switch viewRouter.currentPage {
case .page1:
ContentViewA(viewRouter: viewRouter)
case .page2:
ContentViewB(viewRouter: viewRouter)
}
Then, update the Button’s action parameter for showing the first page again:
Button(action: {
viewRouter.currentPage = .page1
}) {
BackButtonContent()
}
We can now navigate independently between our ContentViews!
Recap š
We just figured out how to navigate between different views using an @ObservableObject. We created a ViewRouter and bound our MotherView and the ContentViews to it. Then, we achieved to manipulate the ViewRouterās currentPage property when clicking on the ContentViews “Next”-Buttons. Due to the @Published property wrapperās functionality, this causes the MotherView to rebuild its body with eventually hosting the right ContentView!Ā
But often, there is an alternative, more efficient way to do this: Using an @EnvironmentObject. EnvironmentObjects provide us with more freedom and independence within our appās view hierarchy. You will see what we mean by that when reading on.
By the way: we are also going to learn how to apply some nice transition animations to our app!
12 replies on “How to navigate independently using @ObservableObjects and @StateObjects”
NavigatingInSwiftUI(1) project doesn’t download here
Thanks your comment, just fixed it!
C8PageStateChange.mp4 and C8NextButton.mp4 do not play on my iPad.
Please make it playable on my iPad since I am using it as my second screen.
https://whatismybrowser.com/w/J2TE4E2
Hi, I’m sorry but I’m unable to reproduce this behaviour. Did you make sure no content/ad blockers are running?
In ViewRouter.swift, autocomplete suggested to inherit class ObservedObject instead of ObservableObject. You can improve the readability of your tutorial by warning about this possible misuse.
By the way, can you tell me the difference between these two?
Hi Richard, while ObservableObject is a protocol used to make a class observable as in our example, @ObservedObjects are used to actually observe these classes. Actually, they are quite similar to @StateObjects and basically follow the same behaviour. Take a look at this Stackoverflow post for further information.
Thank you! I had this issue. I was wondering whats wrong.
Hi, Thank you for this this lesson! I tried to implement this in a project I’m building and seem to have run into an issue. One of the pages I wanted to use this with always seems to return errors and will not work! I tried it with another page and it worked. Is there something I might be missing or something that might cause it to fail? Thanks
Hi Jon,
could you send me your project folder mailing at contact@blckbirds.com?
It seems some tutorials recommend using @Stateobject in the upper hierarchy view while using @ObservedObject in lower hierarchy views.
However, you used @StateObjects for MotherView and ContentView A and B.
I’m kind of confused… when should I use StateObject and ObservedObject?
In a nutshell: When a View has an @ObservedObject, every time the view gets refreshed it creates a new instance of this @ObservedObject. When using a @StateObject instead, the created instance will be kept/reused whenever the view gets refreshed.I recommend you to use a StateObject in case of doubt. Check this article for more information.
Hi:
1. I used only @State in the MotherView and @Binding in ContentViewA and ContentViewB, and it worked. I knew of course it’s only for illustration to use @StateObject here, just for you guys’ reference.
2. In my humble opinion, I thought @ObservedObject is to the @StateObject what @Binding is to the @State, so I used @StateObject in the MotherView and then passed the object to the ContentViewA and ContentViewB, and I use @ObservedObject in the later two views.
struct MotherView: View {
@StateObject var viewRouter = ViewRouter()
…
var body: some View {
…
ContentViewA(viewRouter: viewRouter)
…
ContentViewB(viewRouter: viewRouter)
…
}
}
strunct ContentViewA: View {
@ObservedObject var viewRouter: ViewRouter
…
}
3. As the example code above illustrated, I initiated a new ViewRouter right inside MotherView instead of passed from the NavigatingInSwiftUIApp, and it worked.
Although it worked, I wondered if there was something bad about my implementation. Could you please give me some advice?
Thanks for your wonderful tutorial.