Unlocking the Power of UIKit Navigation in SwiftUI
I was creating a new project in swiftUI and the navigation part was a challenge, especially putting all the navigation in one place. After researching I found that most people use UIKit navigation there were also other materials in navigation but they all used navigation stack but needed support for iOS 14 so I decided to use the UIkit navigation pattern.
The concept is to wrap the view with a view controller and use the navigation functions we use in UIKit.
import SwiftUI
public protocol Router {
associatedtype V: View
@ViewBuilder
func view() -> V
}
import SwiftUI
enum AppRoute: Router {
case firstScreen
case secondScreen(number: Int)
case thirdScreen(number: Int)
@ViewBuilder
func view() -> some View {
switch self {
case .firstScreen:
ExampleView()
case .secondScreen(let number):
SecondExampleView(numb: .constant(number))
case .thirdScreen(let number):
ThirdExampleView(numb: .constant(number))
}
}
}
The AppRoute
contains all the screens in the app it should follow the Router
protocol to ensure that it has a view()
that returns a view.
Next is the navigation
import SwiftUI
class AppNavigation {
static var shared = AppNavigation(startingRoute: .firstScreen)
let startingRoute: AppRoute
init(navigationController: UINavigationController = .init(), startingRoute: AppRoute) {
self.startingRoute = startingRoute
}
func startingViewController() -> UIViewController{
let view = startingRoute.view()
let navigationController: UINavigationController = .init()
let viewWithCoordinator = view.environmentObject(navigationController)
let viewController = UIHostingController(rootView: viewWithCoordinator)
navigationController.setViewControllers([viewController], animated: false)
return navigationController
}
func present(_ route: AppRoute, animated: Bool = true, source: UINavigationController) {
let view = route.view()
let destinationNavigationController: UINavigationController = .init()
let viewWithNavigator = view.environmentObject(destinationNavigationController)
let viewController = UIHostingController(rootView: viewWithNavigator)
destinationNavigationController.modalPresentationStyle = .fullScreen
destinationNavigationController.setViewControllers([viewController], animated: animated)
source.present(destinationNavigationController, animated: animated)
}
func navigate(_ route: AppRoute, animated: Bool = true, source: UINavigationController) {
let view = route.view()
let viewWithNavigator = view.environmentObject(source)
let viewController = UIHostingController(rootView: viewWithNavigator)
source.showCustomBackButton()
source.pushViewController(viewController, animated: animated)
}
func presentModally(_ route: AppRoute, animated: Bool = true, source: UINavigationController) {
let view = route.view()
let destinationNavigationController: UINavigationController = .init()
let viewWithNavigator = view.environmentObject(destinationNavigationController)
let viewController = UIHostingController(rootView: viewWithNavigator)
destinationNavigationController.modalPresentationStyle = .formSheet
destinationNavigationController.setViewControllers([viewController], animated: animated)
source.present(destinationNavigationController, animated: animated)
}
}
In these functions, we are creating a view controller from the View using UIHostingController
before creating the view controller we will pass the navigation controller of that view controller as an environment object so we can access the Navigation controller from inside the view.
import UIKit
extension UINavigationController: ObservableObject {
func navigateTo(route: AppRoute) {
AppNavigation.shared.navigate(route, source: self)
}
func presentScreen(route: AppRoute) {
AppNavigation.shared.present(route, source: self)
}
func pop() {
popViewController(animated: true)
}
func popToRoot() {
popToRootViewController(animated: true)
}
}
I am adding the navigation in the navigation controller for easily accessing it in the view. UINavigationController
conforms to the ObservableObject
only then we can pass it as an environment object.
import SwiftUI
struct SecondExampleView: View {
@EnvironmentObject var navigator: UINavigationController
@Binding var numb: Int
var body: some View {
Form {
Text("screen number \(numb)")
Button("dismiss") {
navigator.dismiss(animated: true)
}
Button("next screen present") {
navigator.presentScreen(route: .secondScreen(number: numb + 1))
}
Button("next screen push") {
navigator.navigateTo(route: .thirdScreen(number: numb + 1))
}
Button("pop to root") {
navigator.popToRoot()
}
Button("pop") {
navigator.pop()
}
}
.navigationTitle("secondView")
}
}
This is an example view where we get the navigation controller as an Environment object and call the navigation function we need by passing in the destination screen.