Tutorial · 25 minutes
Your first Scaffolding project.
Build a three-screen iOS app — list, detail, and settings — using
a single FlowCoordinatable. By the end you'll have
push navigation, a sheet, and a coordinator that the macro
generates routes for automatically.
01 Set up the project
Create a new iOS App project in Xcode. Choose SwiftUI for the interface and name it ScaffoldingDemo.
Add Scaffolding via File → Add Package Dependencies, enter https://github.com/dotaeva/scaffolding.git, and add
the Scaffolding library to your app target.
import SwiftUI
@main
struct ScaffoldingDemoApp: App {
var body: some Scene {
WindowGroup {
Text("Coming soon")
}
}
}02 Create your coordinator
Define a FlowCoordinatable that manages navigation
between three screens — Home, Detail, and Settings. Mark it @Scaffoldable @Observable; the macro inspects each
function and generates the type-safe Destinations enum at compile time.
Start with an empty class and a FlowStack rooted at .home — a case the macro will generate from the home() function you add next.
import SwiftUI
import Scaffolding
@MainActor @Observable @Scaffoldable
final class AppCoordinator: @MainActor FlowCoordinatable {
var stack = FlowStack<AppCoordinator>(root: .home)
}Add a home() function that returns some View. This becomes the stack's root destination
and the macro generates a .home enum case from the
function name.
@MainActor @Observable @Scaffoldable
final class AppCoordinator: @MainActor FlowCoordinatable {
var stack = FlowStack<AppCoordinator>(root: .home)
func home() -> some View { Text("Home") }
}Add a detail(title:) function with a parameter — the
macro generates a .detail(title:) enum case with a
matching associated value.
@MainActor @Observable @Scaffoldable
final class AppCoordinator: @MainActor FlowCoordinatable {
var stack = FlowStack<AppCoordinator>(root: .home)
func home() -> some View { Text("Home") }
func detail(title: String) -> some View { Text(title) }
}Add settings() to round out the routes. You now have
three: .home, .detail(title:), and .settings — generated automatically.
@MainActor @Observable @Scaffoldable
final class AppCoordinator: @MainActor FlowCoordinatable {
var stack = FlowStack<AppCoordinator>(root: .home)
func home() -> some View { Text("Home") }
func detail(title: String) -> some View { Text(title) }
func settings() -> some View { Text("Settings") }
}03 Build the views
Each screen is a regular SwiftUI view that resolves its
coordinator via @Environment — Scaffolding injects
every coordinator in the hierarchy automatically.
import SwiftUI
import Scaffolding
struct HomeView: View {
@Environment(AppCoordinator.self) private var coordinator
var body: some View {
Text("Hello, \(String(describing: coordinator))")
}
}struct DetailView: View {
let title: String
var body: some View { Text(title).font(.title) }
}struct SettingsView: View {
@Environment(AppCoordinator.self) private var coordinator
var body: some View {
Form {
Button("Done") {
coordinator.pop()
}
}
.navigationTitle("Settings")
}
}SettingsView calls coordinator.pop() to dismiss itself — modals share the same FlowStack.destinations array as pushes, so popping
the topmost destination removes the sheet.
05 Present a sheet
Replace the placeholder views with the real ones, then add a
toolbar button to HomeView that presents settings as
a sheet. Use present(_:as:) — route(to:) is push-only.
@MainActor @Observable @Scaffoldable
final class AppCoordinator: @MainActor FlowCoordinatable {
var stack = FlowStack<AppCoordinator>(root: .home)
func home() -> some View { HomeView() }
func detail(title: String) -> some View { DetailView(title: title) }
func settings() -> some View {
NavigationStack { SettingsView() }
}
}struct HomeView: View {
@Environment(AppCoordinator.self) private var coordinator
let items = ["Mercury", "Venus", "Earth", "Mars"]
var body: some View {
List(items, id: \.self) { item in
Button {
coordinator.route(to: .detail(title: item))
} label: { Label(item, systemImage: "globe") }
}
.navigationTitle("Planets")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
coordinator.present(.settings, as: .sheet)
} label: {
Image(systemName: "gear")
}
}
}
}
}Pass .fullScreenCover in place of .sheet for a full-screen modal. Both live in the same FlowStack.destinations array as the pushed
destinations.
With everything wired, this is what the user gets — push a planet, tap the gear for settings, dismiss to come back:
06 Wire up the app
Connect the coordinator to your app entry point. Read the coordinator.view property inside WindowGroup — it's a computed property (no
parentheses) that renders the entire navigation hierarchy as a
SwiftUI view.
import SwiftUI
@main
struct ScaffoldingDemoApp: App {
@State private var coordinator = AppCoordinator()
var body: some Scene {
WindowGroup {
coordinator.view
}
}
}Build and run. You'll see a list of planets — tap one to push the detail screen, tap the gear to present settings as a sheet, tap Done to dismiss it.