SwiftUI Router Router
  • July 28, 2025

SwiftUI Router

Router


 

Router is a library that assists with SwiftUI view transitions.

Installation


.package(name: “Router”, url: “git@github.com:1amageek/Router.git”, .upToNextMajor(from: “0.2.0”)),

Usag


Router

The Router specifies the View to be navigated. The argument of Router is the Path of the first View to be displayed. By default, / is specified.

Route

Route will show the View of the Path specified in the argument. Path has placeholders and the parameters can be accessed from context.

import SwiftUI
import Router

struct ContentView: View {

@State var isShow: Bool = false

var body: some View {
Router(“/weather”) {
Route(“/weather”) {
ListView()
}
Route(“/weather/{weatherLabel}”) { context in
DetailView(label: context.paramaters[“weatherLabel”]!)
}
}
.environmentObject(DataStore())
}
}

Navigator

It transitions between screens by giving Navigator a path. You can specify the transition animation. In the example below, we call the push animation.

struct ListView: View {

@Environment(\.navigator) private var navigator: Binding<Navigator>

@EnvironmentObject var dataStore: DataStore

var body: some View {

List {
Section(header:
Text(“Weather”)
.font(.system(size: 24, weight: .black, design: .rounded))
.padding()
) {
ForEach(dataStore.data, id: \.label) { data in
Button(action: {
navigator.push {
navigator.wrappedValue.path = “/weather/\(data.label)”
}
}) {
Label(data.title, systemImage: data.systemImage)
.font(.system(size: 20, weight: .bold, design: .rounded))
Spacer()
}
.buttonStyle(PlainButtonStyle())
}
}
}
.listStyle(InsetGroupedListStyle())
}
}

Navigator is defined as an environment, so it can be called from anywhere.

struct DetailView: View {

@Environment(\.navigator) private var navigator: Binding<Navigator>

@EnvironmentObject var dataStore: DataStore

var label: String

var weather: Weather? {
return self.dataStore.data.filter({$0.label == self.label}).first
}

var body: some View {
ZStack {
VStack(spacing: 10) {
Image(systemName: self.weather!.systemImage)
.font(.system(size: 120, weight: .bold, design: .rounded))
Text(label)
.font(.system(size: 30, weight: .bold, design: .rounded))
}
VStack(alignment: .leading) {
HStack {
Button(action: {
navigator.pop {
navigator.wrappedValue.path = “/weather”
}
}) {
Image(systemName: “chevron.backward”)
.font(.system(size: 20, weight: .bold, design: .rounded))
}
.buttonStyle(PlainButtonStyle())

Spacer()
}
Spacer()
}
.padding()
}
}
}

Custom Transition Animation


To customize the transition animations, you must first extend AnyTransition.

public extension AnyTransition {

struct NavigationFrontModifier: ViewModifier {
let offset: CGSize
public func body(content: Content) -> some View {
ZStack {
Color(UIColor.systemBackground)
content
}
.offset(offset)
}
}

static var navigationFront: AnyTransition {
AnyTransition.modifier(
active: NavigationFrontModifier(offset: CGSize(width: UIScreen.main.bounds.width, height: 0)),
identity: NavigationFrontModifier(offset: .zero)
)
}

struct NavigationBackModifier: ViewModifier {
let opacity: Double
let offset: CGSize
public func body(content: Content) -> some View {
ZStack {
content
.offset(offset)
Color.black.opacity(opacity)
}
}
}

static var navigationBack: AnyTransition {
AnyTransition.modifier(
active: NavigationBackModifier(opacity: 0.17, offset: CGSize(width: -UIScreen.main.bounds.width / 3, height: 0)),
identity: NavigationBackModifier(opacity: 0, offset: .zero)
)
}
}

Next, we will extend Binding.

public extension Binding where Value == Navigator {

func push<Result>(_ animation: Animation? = .default, _ body: () throws -> Result) rethrows -> Result {
let insertion: AnyTransition = .navigationFront
let removal: AnyTransition = .navigationBack
let transition: AnyTransition = .asymmetric(insertion: insertion, removal: removal)
self.wrappedValue.zIndex = 0
self.wrappedValue.transition = transition
self.wrappedValue.uuid = UUID()
return try withAnimation(animation, body)
}

func pop<Result>(_ animation: Animation? = .default, _ body: () throws -> Result) rethrows -> Result {
let insertion: AnyTransition = .navigationBack
let removal: AnyTransition = .navigationFront
let transition: AnyTransition = .asymmetric(insertion: insertion, removal: removal)
self.wrappedValue.zIndex = 1
self.wrappedValue.transition = transition
self.wrappedValue.uuid = UUID()
return try withAnimation(animation, body)
}
}

It can be called as follows

navigator.push {
navigator.wrappedValue.path = “/weather/\(data.label)”
}

navigator.pop {
navigator.wrappedValue.path = “/weather”
}

GitHub


View Github

YOU MIGHT ALSO LIKE...
How to take action when a property changes

1. Taking Action When a Property Changes: Property Observers Swift lets you observe and respond to changes in a property’s ...

How to create your own structs? How to compute property values dynamically?

1. Creating Your Own Structs In Swift, a struct is a value type that you define with the struct keyword. ...

How to use trailing closures and shorthand syntax?

1. Trailing Closure Syntax When the last parameter to a function is a closure, you can write that closure after ...

How to create and use closures?

1. What Is a Closure (and Why Swift Loves Them) A closure in Swift is a self-contained block of functionality ...

How to provide default values for parameters How to handle errors in functions

1. Providing Default Values for Function Parameters (Deep Dive) 1.1 Syntax and Ordering Declaration You assign a default right in ...