A new perspective on SwiftUI navigation

Mijick
5 min readMay 30, 2024

--

Maintaining proper navigation system is crucial for any scalable project. The first versions of SwiftUI didn’t make it easy for us in this regard; routing system was very complicated, buggy, and required maintaining a lot of unnecessary code. Over the past two years, the situation has somewhat improved with the introduction of iOS 16 and an enhanced navigation system it brought.

In our opinion, this change was insufficient, which prompted us to develop an alternative to the native NavigationView that currently powers our applications. In this article, we want to present a thorough analysis of the library — starting from implementation details, presenting both the advantages and disadvantages of its application, and sharing our opinion on which projects it should be suitable for and in which ones it is better to stick with the default solution. As usual, I suggest you brew yourself a cup of tea, grab some biscuits and we can get started!

Let’s create an app

We’ll start this paragraph by creating a simple application, the code of which you can find here (by the way, if you want to receive notifications about our future libraries, we encourage you to follow us on GitHub).

Here’s how it works, using the native NavigationView:

Navigation with a native solution

And now the same application, powered by MijickNavigationView:

Navigation powered by Mijick

As we can see in the video, there aren’t many differences in how it looks (except for the advanced transition animations in MijickNavigationView). The real magic, however, begins when we delve into the code.

// PUSH: NavigationView
func createProfileButton() -> some View { NavigationLink(destination: destination) {
Text("PJ")
.font(.bold(14))
.foregroundStyle(.onBackground75)
.kerning(-0.25)
.frame(width: height, height: height)
.background(Circle().applyDefaultBackgroundAndStroke())
}}
var destination: some View { ProfileView() }
// PUSH: MijickNavigationView
func createProfileButton() -> some View { Button(action: onProfileButtonTap) {
Text("PJ")
.font(.bold(14))
.foregroundStyle(.onBackground75)
.kerning(-0.25)
.frame(width: height, height: height)
.background(Circle().applyDefaultBackgroundAndStroke())
}}
func onProfileButtonTap() { ProfileView().push(with: .cubeRotation) }

There aren’t too many differences, are there? It’s all true, but with MijickNavigationView, you can push the view from anywhere in your code (even from inside ViewModel!). Meanwhile, let’s continue and see how we can return to the previous view:

// POP: NavigationView
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

func createCloseButton() -> some View {
Button(action: presentationMode.wrappedValue.dismiss) {
Image(.close)
.resizable()
.foregroundStyle(.onBackground)
.frame(width: 28, height: 28)
}
}
// POP: MijickNavigationView
func createCloseButton() -> some View { Button(action: pop) {
Image(.close)
.resizable()
.foregroundStyle(.onBackground)
.frame(width: 28, height: 28)
}}

The situation is gradually becoming more intriguing. The native option forces us to declare a completely unnecessary attribute, cluttering the code a bit. Let’s now try to go back not to the previous element, but to the root:

// POP TO ROOT: NavigationView


// Root Screen
@State var isViewActive: Bool = false

func createDestinationButton() -> some View {
NavigationLink(isActive: $isViewActive, destination)
}
var destination: some View { Screen1(isViewActive: $isViewActive) }


// Screen(1...n-1)
@Binding var isViewActive: Bool

func createDestinationButton() -> some View {
NavigationLink(isActive: $isViewActive, destination)
}
var destination: some View { ScreenN(isViewActive: $isViewActive) }

// ScreenN
@Binding var isViewActive: Bool
func createCloseButton() -> some View {
Component.Button.Primary(text: "Close", action: { isViewActive = false } )
}
// POP TO ROOT: MijickNavigationView
func createCloseButton() -> some View {
Component.Button.Primary(text: "Close", action: popToRoot)
}

Oh boi, we had to make the code really messy to accomplish such a simple and often used option! The situation becomes even more complicated when we try to return to the selected view or change the root, but I won’t cover this scenario within this article.

How does this actually work?

In this paragraph, we will focus on the technical details of how the library is implemented. The concept is pretty simple — we have a NavigationStack where we place subsequent views. The view we see on the screen is at the top of the stack and the root view is at its bottom. A method called push adds another element to the stack, while pop removes n elements from it.

The part responsible for handling animations, is a bit more complex and required us to apply a few tricks to overcome the limits of SwiftUI.

  1. Firstly, we don’t use elements from the NavigationManager, we copy the elements instead. Why so? In a nutshell, because are applied to two specific elements in the stack, and using the views directly from NavigaitonManager would pose the risk of certain bugs that we encountered during our implementation process.
  2. Secondly, we chose not to use the transition method because it gave us too limited animation options, and also caused some nasty bugs.
  3. Thirdly, all views in the stack are ‘active’, which presents some challenges and requires some experience and discipline from the developer, but provides some opportunities that were very important to us (such as saving the scrollview position during navigation).

Who can benefit from using this library?

I could have written “to everyone” and ended this article, but the truth is a bit more complicated. MijickNavigationView offers developers several interesting options, however, it requires some experience in design patterns, code organization, and knowledge of performance optimatization in SwiftUI. I would therefore advise against using this library for people who are new to programming as it poses a significant risk of developing poor coding habits and/or creating applications that are inefficient in terms of performance.

Summary

While SwiftUI is still a relatively new and imperfect language, it presents numerous possibilities and greatly enhances productivity compared to UIKit. An evident illustration of its early stage is NavigationView, which offers only fundamental functionalities and can make the code cluttered. Today we learned how we can unleash the potential of navigation using an external library. If you have any ideas for features that we could add to our library, feel free to create an issue. Have a great day, thank you for stopping by and see you in our future articles. Stay tuned! 🖖

By Tomasz K.

Hey, before you go!

We are a young organisation of people developing open-source software. If you would like to support us and stay up to date with our new content:

  • Please consider clapping and following us on Medium 👏
  • Follow us X | GitHub
  • Say hello to us at team@mijick.com

--

--