In this blog, we will explore routing in SwiftUI, a concept that allows navigation between different views or screens in your app. Routing is essential for building seamless and interactive user experiences, enabling users to move through various parts of your app intuitively.
We will demonstrate how to create a custom routing system in SwiftUI by implementing a reusable solution using an AppRoute
enum and a Router
class. This approach provides more control and flexibility than the built-in navigation tools like NavigationLink
and sheet
What You Will Learn
- The Basics of Routing: Understand the concept of routing and its importance in mobile app development
- Implementing a Custom Router: Learn how to create a
Router
class to manage navigation state dynamically. - Enum-Based Routes: Discover how the
AppRoute
enum simplifies navigation by defining app-specific screens. - Dynamic View Switching: See how to render different views based on the current route.
- Practical Navigation Methods: Implement essential navigation methods like
push
,pop
,popTo
, andreset
for managing the navigation stack.
Custom Basic Navigation
enum AppRoute: Hashable, Codable {
case first
case second
case third
case fourth
case fifth
}
class Router: ObservableObject {
@Published private var stack: [AppRoute] = [.first]
func push(_ screen: AppRoute) {
stack.append(screen)
}
func pop() -> AppRoute? {
return stack.popLast()
}
func popTo(_ route: AppRoute) {
if let index = stack.firstIndex(of: route) {
stack = Array(stack.prefix(upTo: index + 1))
}
}
func currentScreen() -> AppRoute {
return stack.last!
}
func reset(to route: AppRoute) {
stack = [route]
}
}
AppRoute
Enum: The AppRoute
enum defines the screens in the app as cases. Each case represents a specific screen, making the navigation logic type-safe and easy to manage:
Router
Class: The Router
class is an ObservableObject
that manages a navigation stack using an array of AppRoute
. This stack-based approach allows for pushing, popping, and resetting routes dynamically. Key methods include:
push(_:)
: Adds a new screen to the stack.pop()
: Removes the last screen from the stack.popTo(_:)
: Pops to a specific screen in the stack.reset(to:)
: Resets the stack to a single route.currentScreen()
: Returns the current screen for view rendering.
@main
struct RoutingInSwiftUIApp: App {
@StateObject private var navigationStack = Router()
var body: some Scene {
WindowGroup {
NavigationStack {
VStack {
switch navigationStack.currentScreen() {
case .first:
FirstScreen()
case .second:
SecondScreen()
case .third:
ThirdScreen()
case .fourth:
FourthScreen()
case .fifth:
FifthScreen()
}
}
}
.environmentObject(navigationStack)
}
}
}
RoutingInSwiftUIApp
: The RoutingInSwiftUIApp
structure initializes the Router
and renders the appropriate screen based on the current route. The navigation stack is integrated using SwiftUI’s @StateObject
and the .environmentObject
modifier:
First Screen
import SwiftUI
struct FirstScreen: View {
@EnvironmentObject var navigationStack: Router
var body: some View {
ZStack {
Color.red
.ignoresSafeArea()
VStack {
Button(action: {
navigationStack.push(.second)
}) {
Text("First Screen")
.font(.largeTitle)
.bold()
}
}
Spacer()
}
}
}
#Preview {
FirstScreen()
}
Second Screen
import SwiftUI
struct SecondScreen: View {
@EnvironmentObject var navigationStack: Router
var body: some View {
ZStack {
Color.orange
.ignoresSafeArea()
VStack {
HStack {
Button {
_ = navigationStack.pop()
} label: {
Image("back")
.resizable()
.frame(width: 25, height: 25)
.padding(.leading)
}
Spacer()
}
.padding(.top, 20)
Spacer()
}
VStack{
Button(action: {
navigationStack.push(.third)
}) {
Text("Second Screen")
.font(.largeTitle)
.bold()
}
}
}
}
}
#Preview {
SecondScreen()
}
Third Screen
import SwiftUI
struct ThirdScreen: View {
@EnvironmentObject var navigationStack: Router
var body: some View {
ZStack {
Color.green
.ignoresSafeArea()
VStack {
HStack {
Button {
_ = navigationStack.pop()
} label: {
Image("back")
.resizable()
.frame(width: 25, height: 25)
.padding(.leading)
}
Spacer()
}
.padding(.top, 20)
Spacer()
}
VStack {
Button(action: {
}) {
Text("Third Screen")
.font(.largeTitle)
.bold()
}
}
}
}
}
#Preview {
ThirdScreen()
}
Passing Parameters Between Screens
To pass parameters, we’ll update the AppRoute
enum to include associated values.
enum AppRoute: Hashable, Codable {
case first
case second(userName: String)
case third(message: String)
}
Modified Router Logic
Update the switch-case in RoutingInSwiftUIApp
to handle associated values.
@main
struct RoutingInSwiftUIApp: App {
@StateObject private var navigationStack = Router()
var body: some Scene {
WindowGroup {
NavigationStack {
VStack {
switch navigationStack.currentScreen() {
case .first:
FirstScreen()
case .second((let userName):
SecondScreen(userName: userName)
case .third:
ThirdScreen(message: message)
}
}
}
.environmentObject(navigationStack)
}
}
}
Passing Data into Screen
SecondScreen
struct SecondScreen: View {
@EnvironmentObject var navigationStack: Router
var userName: String
var body: some View {
ZStack {
Color.orange.ignoresSafeArea()
VStack {
Text("Welcome, \(userName)")
.font(.largeTitle)
Button("Go to Third Screen") {
navigationStack.push(.third(message: "Hello from Second Screen"))
}
}
}
}
}
ThirdScreen
struct ThirdScreen: View {
@EnvironmentObject var navigationStack: Router
var message: String
var body: some View {
ZStack {
Color.green.ignoresSafeArea()
VStack {
Text(message)
.font(.title)
Button("Back to First") {
navigationStack.popTo(.first)
}
}
}
}
}
SwiftUI Routing: Pop to Any Screen
For large applications with 40+ screens, simplicity and flexibility are key. Instead of over-complicating navigation, we’ll implement an easy-to-use routing system that allows direct navigation between any screens. This approach focuses on maintainability and avoids deep stack manipulation.
Simplified Routing System
The AppRoute
enum will define all screens in your app.
We’ll enhance the Router
class to include a navigation stack and a popTo(_:)
method that allows popping back to any screen.
func popTo(to target: AppRoute) {
guard !stack.isEmpty else { return }
while let last = stack.last, last != target {
stack.removeLast()
}
}
FifthScreen with popTo
Implementation
This screen demonstrates how to navigate back to any specific screen using popTo
.
import SwiftUI
struct FifthScreen: View {
@EnvironmentObject var navigationStack: Router
var body: some View {
ZStack {
Color.secondary
.ignoresSafeArea()
VStack {
Button(action: {
navigationStack.popTo(to: .first)
}) {
Text("Go back to 1 screen")
.font(.title)
.bold()
}
Spacer()
Button(action: {
navigationStack.popTo(to: .second)
}) {
Text("Go back to 2 screen")
.font(.title)
.bold()
}
Spacer()
Button(action: {
navigationStack.popTo(to: .third)
}) {
Text("Go back to 3 screen")
.font(.title)
.bold()
}
Spacer()
Button(action: {
navigationStack.popTo(to: .fourth)
}) {
Text("Go back to 4 screen")
.font(.title)
.bold()
}
}
.padding(.vertical, 150)
Spacer()
}
}
}
#Preview {
FifthScreen()
}
Leave a Reply