Photo by Alexander Andrews on Unsplash

How to handle navigation in SwiftUI

Mike Haidan
3 min readSep 6, 2021

--

The 3rd version of SwiftUI has been released and we still don’t have an answer. How to remove navigation from the UI layer?

If you tried any complex navigation in SwiftUI, you may noticed, that navigation is “tightly tied” in the UI layer, what brings these problems:

  • Blurred boundaries between logic and UI.
  • Dynamic navigation issues. Building navigation when an order of the screens depends on different factors (A/B tests, configuration flows, etc.).

I tried different ways to handle navigation in SwiftUI (there are some great articles about navigation in SwiftUI. I highly recommend this one), but, unfortunately, all approaches are faced with basic SwiftUI this principles, you need to add NavigationLink explicitly in the UI code!

This approach adds endless if-else conditions or Boolean properties (or usually both!).

That brings us to the sad truth, need to use UIKit.

BUT, at the end, it’s not so sad, as it sounds. This problem gave an opportunity to build something really flexible. Navigation, using UIKit, but still using SwiftUI under the hood.

Let’s get started! As app architecture was decided to use MVVM+Coordinator pattern. It works really well in the SwiftUI ecosystem. (I won’t describe the Coordinator pattern in the article. There are a lot of amazing articles already :) ).

Let’s start from Coordinator protocol:

  1. Child coordinators of “self”. The array is not empty, if the Coordinator has children.
  2. Parent coordinator of “self”. The property is not nil, if the Coordinator has a parent.
  3. Root controller of the Coordinator (e.g. it may be UINavigationController, UITabBarController).

Let’s got to the ViewModel:

  1. ViewModel’s coordinator. Each ViewModel has to have a link to a Coordinator.
  2. Setup method, where we are making some initial setup (e.g. API calls, data source setup, etc.).

And View itself:

  1. Initialization method for a View. Mostly, it should take only ViewModel as a parameter.
  2. A computed property, which transforms a View to a UIViewController (it may be UIHostingViewController for SwiftUI.View).

And final component, a view controller wrapper (it has been developed as abstract class, so each view has to have it’s own implementation):

  1. strong reference to a ViewModel. Note: SwiftUI.View has to have @ObservedObject wrapper instead of @StateObject.
  2. Represents a View, transformed to a UIView object.
  3. It’s not allowed to create an instance of SwiftUIViewController, but need to inherit it instead (an example will be later in the article).
  4. Adding a View as a child UIViewController and pinning it to the edges, using NSLayoutConstraint.

And that’s it. Our setup is ready. By confirming these protocols we can develop a UI using SwiftUI and wrap it into a UIViewController and it’s almost done.

The final result looks like this:

Our final controller is on line 75. As you can seen we don’t need to do any configurations. Everything has been done under the hood. Usually UIViewController should only few lines of code.

You may ask why not to use UIHostingController directly?
There are couple of reasons:

  • A View has all UIViewController lifecycle methods, which add a more flexible setup.
  • This approach forces a developer to use MVVM+C architecture.
  • And the most useful reason, if you’re unable to develop something in SwiftUI, you can easily switch to UIKit. ViewControllerView protocol doesn’t require using SwiftUI. UIViewController can implement ViewControllerView as well. All other components stay untouched.

Regarding last point, here is an example how easy we can switch between SwiftUI and UIKit:

That’s all! I know this approach is not a silver bullet to solve all issues in SwiftUI, but it adds some flexibility and safety of build our apps.

Thank you for reading ❤️.

More detailed implementation you can find here.

Check out my other articles:

Building a UIKit user interface programmatically. Tips & suggestions.
Advanced configuration of UITableViewCell-s

--

--