Scaffolding

API Reference

Symbols.

Every public protocol, type, and macro the Scaffolding module exposes — declaration, conformance, and member topics for each.

Coordinatable

Protocol

@MainActor · base · all coordinator types build on this

The shared surface every coordinator type builds upon: an associated Destinations enum, identity, parent tracking, and the ability to produce a SwiftUI view. You don't conform to Coordinatable directly — use one of the three specialized protocols.

Declaration

Coordinatable.swift
@MainActor
public protocol Coordinatable: AnyObject, Identifiable {
    associatedtype Destinations: Destinationable
        where Destinations.Owner == Self
    associatedtype ViewType: View

    var parent: (any Coordinatable)? { get }
    var view: ViewType { get }
    func customize(_ view: AnyView) -> CustomizeContentView
}

Inherits from

AnyObject, Identifiable

Conforming protocols

FlowCoordinatable, TabCoordinatable, RootCoordinatable

Topics

parent: (any Coordinatable)?
The coordinator that owns this one, or nil at the top of the hierarchy.
view() -> ViewType
Produces the SwiftUI view for this coordinator's hierarchy.
dismissCoordinator()
Removes this coordinator from its parent (pop or modal-dismiss). Tab children log a warning instead.
present(_:as:onDismiss:)
Presents a destination as a sheet or full-screen cover. Available on every coordinator.
customize(_ view: AnyView) -> some View
Override to apply shared modifiers — toolbars, overlays, environment values — to every screen this coordinator renders.
_injectsCoordinator: Bool
Whether this coordinator is injected into descendant @Environment. Defaults to true; opt out with @Scaffoldable(injectsCoordinator: false).

FlowCoordinatable

Protocol

@MainActor · push · pop · sheet · full-screen cover

The push/pop navigation coordinator. Wraps a SwiftUI NavigationStack and stores both pushed screens and modal-presented coordinators in a single FlowStack.destinations array.

Declaration

FlowCoordinatable.swift
@MainActor
public protocol FlowCoordinatable: Coordinatable
    where ViewType == FlowCoordinatableView {

    var stack: FlowStack<Self> { get }
}

Conforms to

Coordinatable

Topics

stack: FlowStack<Self>
Observable container holding root, pushes, and modals.
route(to:onDismiss:) -> Self
Push a destination. Modal presentation is split out — use present(_:as:) for that.
present(_:as:onDismiss:) -> Self
Present a destination as a .sheet or .fullScreenCover. Both end up in stack.destinations with the matching pushType.
pop()
Removes the topmost destination. Works for pushed screens AND modals — they share the same array.
popToRoot()
Drops everything above the root. Each removed destination resolves its onDismiss.
popToFirst(_:) · popToLast(_:)
Pop back to the first / last occurrence of a destination by Destinations.Meta.
setRoot(_:animation:) -> Self
Replace the flow's root destination atomically. Pushed destinations are cleared first.
setRootTransitionAnimation(_:)
Default animation for root swaps.
isInStack(_:) -> Bool
Whether a destination meta is anywhere in the current stack.

TabCoordinatable

Protocol

@MainActor · tab bar · independent stacks per tab

Manages a TabView where each tab is a destination — either a plain view or a child coordinator. Each tab keeps its own independent navigation stack.

Declaration

TabCoordinatable.swift
@MainActor
public protocol TabCoordinatable: Coordinatable
    where ViewType == TabCoordinatableView {

    var tabItems: TabItems<Self> { get }
}

Conforms to

Coordinatable

Topics

tabItems: TabItems<Self>
Observable container holding tabs, selection, and modals.
selectFirstTab(_:) · selectLastTab(_:)
Select by destination meta (first / last match).
select(index:) · select(id:)
Select by zero-based index or tab UUID.
setTabs(_:)
Replace the entire tab list.
appendTab(_:) · insertTab(_:at:)
Add tabs dynamically.
removeFirstTab(_:) · removeLastTab(_:)
Remove by destination meta.
isInTabItems(_:) -> Bool
Whether a destination meta is currently a tab.
setTabBarVisibility(_:)
.automatic · .visible · .hidden
present(_:as:onDismiss:)
Present a modal directly on the tab coordinator — no need to delegate to a child flow.

RootCoordinatable

Protocol

@MainActor · atomic root swap · auth flows

Holds a single root destination that can be swapped wholesale. Ideal for authentication, onboarding, or any state where the entire view hierarchy must change atomically.

Declaration

RootCoordinatable.swift
@MainActor
public protocol RootCoordinatable: Coordinatable
    where ViewType == RootCoordinatableView {

    var root: Root<Self> { get }
}

Conforms to

Coordinatable

Topics

root: Root<Self>
Observable container with the current root + presented modals.
setRoot(_:animation:) -> Self
Atomic root swap. Fires onDismiss on the previous tree.
isRoot(_:) -> Bool
Whether the current root matches a given destination meta.
setRootTransitionAnimation(_:)
Default animation for root swaps.
present(_:as:onDismiss:)
Present a modal directly on the root coordinator.

FlowStack<Coordinator>

Class

@MainActor · @Observable · final

Backs a FlowCoordinatable. The single destinations array holds all destinations on the flow — pushes and modals — so pop() on the parent flow can dismiss a sheet when that sheet is the topmost destination.

Declaration

FlowStack.swift
@MainActor @Observable
public final class FlowStack<Coordinator: FlowCoordinatable> {
    public var root: Destination?
    public var destinations: [Destination] = []   // pushes + modals
    public var animation: Animation? = .default
    public var presentedAs: PresentationType?
    public weak var parent: (any Coordinatable)?
}

Used by

FlowCoordinatable

Topics

root: Destination?
The flow's root destination.
destinations: [Destination]
Mixed array of pushes and modals (filtered by pushType at render time).
animation: Animation?
Default animation for root transitions.
presentedAs: PresentationType?
How this flow itself is presented (when nested).
parent: (any Coordinatable)?
Weak reference to the parent coordinator.

Root<Coordinator>

Class

@MainActor · @Observable · final

Backs a RootCoordinatable. modals holds modals presented directly from the root coordinator (added via present(_:as:) on the root).

Declaration

Root.swift
@MainActor @Observable
public final class Root<Coordinator: RootCoordinatable> {
    public var root: Destination?
    public var animation: Animation? = .default
    public var presentedAs: PresentationType?
    public var modals: [Destination] = []
}

Used by

RootCoordinatable

TabItems<Coordinator>

Class

@MainActor · @Observable · final

Backs a TabCoordinatable. modals holds modals presented directly from the tab coordinator.

Declaration

TabItems.swift
@MainActor @Observable
public final class TabItems<Coordinator: TabCoordinatable> {
    public var tabs: [Destination] = []
    public var selectedTab: UUID? = nil
    public var tabBarVisibility: Visibility = .automatic
    public var modals: [Destination] = []
}

Used by

TabCoordinatable

Destination

Struct

Identifiable · Hashable

A resolved navigation destination wrapping a view or child coordinator together with routing metadata. The generated Destinations enum produces these via its value(for:) method — you rarely create them by hand.

Declaration

Destination.swift
public struct Destination: Identifiable, Hashable {
    public var id: UUID
    public var routeType: DestinationType
    public var presentationType: DestinationType
    public let meta: any DestinationMeta
}

Topics

id: UUID
Stable identifier for this destination instance.
routeType: DestinationType
How this destination was originally routed (root / push / sheet / fullScreenCover).
presentationType: DestinationType
The effective presentation type, derived from pushType.
meta: any DestinationMeta
Type-erased identity that matches the macro-generated Destinations.Meta case.

Destination type enums

Enums

3 public enums · used together at the routing boundary

Three small enums classify destinations from different angles — their lifecycle origin, the internal presentation tag, and the public API surface for modal presentation.

Declaration

Destination.swift
public enum DestinationType {
    case root, push, sheet, fullScreenCover
}

public enum PresentationType {
    case push, sheet, fullScreenCover
}

public enum ModalPresentationType {
    case sheet, fullScreenCover
}

Topics

DestinationType
Lifecycle origin: .root · .push · .sheet · .fullScreenCover. Read from Destination.routeType.
PresentationType
Internal tag: .push · .sheet · .fullScreenCover. Stored on Destination.pushType.
ModalPresentationType
Public modal API: .sheet · .fullScreenCover. Pass to present(_:as:).

The two modal kinds animate from the bottom; tapping the backdrop on a sheet — or calling pop() on the parent flow — dismisses them.

Destinationable & DestinationMeta

Protocols

macro-implemented · you never conform manually

Destinationable bridges between a coordinator's generated Destinations enum and the runtime Destination value. DestinationMeta identifies a case without its associated values — useful for popToFirst(_:), selectFirstTab(_:), and similar APIs.

Declaration

Destination.swift
@MainActor
public protocol Destinationable {
    associatedtype Meta: DestinationMeta
    associatedtype Owner

    var meta: Meta { get }
    func value(for instance: Owner) -> Destination
}

@MainActor
public protocol DestinationMeta: Equatable { }

Conformance

Synthesised by @Scaffoldable — you don't implement these protocols by hand.

@Scaffoldable

Macro

attached(member) · generates Destinations + _injectsCoordinator

Apply to an @Observable class that conforms to FlowCoordinatable, TabCoordinatable, or RootCoordinatable. The macro inspects every function whose return type is some View, any Coordinatable, or a supported tuple, and synthesises a Destinations enum with one case per eligible function.

Declaration

Scaffolding.swift
@attached(member, names: named(Destinations), named(_injectsCoordinator))
public macro Scaffoldable(injectsCoordinator: Bool = true)
    = #externalMacro(module: "ScaffoldingMacros", type: "ScaffoldableMacro")

Auto-tracked return types

some View
A view destination — pushed onto the stack or rendered as the active root / modal.
any Coordinatable
A child-coordinator destination. Must be the existential any Coordinatable — concrete coordinator types like -> LoginCoordinator are not recognised by the macro.
(any Coordinatable, some View)
A tab tuple — coordinator content + label. Used by TabCoordinatable.
(some View, some View)
A view-only tab tuple — content + label, no child coordinator.
(any Coordinatable, TabRole) · (some View, TabRole) · (any Coordinatable, some View, TabRole) · (some View, some View, TabRole)
iOS 18+ tab tuples that include a TabRole.

Parameters

injectsCoordinator: Bool = true
Whether the coordinator should be injected into descendant @Environment. Pass false on reusable views that shouldn't bind to a specific flow.

Generates

Destinations enum (conforms to Destinationable) · _injectsCoordinator property

@ScaffoldingIgnored

Macro

attached(peer) · explicit exclude

Excludes a single function from the generated Destinations enum. Apply it whenever a method's signature looks like a route to the macro — i.e. returns one of the auto-tracked return types — but is actually a helper, an override, or a factory you don't want surfaced as a destination case.

When to use it

customize(_ view: AnyView) -> some View
Returns some View, so without the attribute the macro would generate a .customize(view:) destination. Mark it @ScaffoldingIgnored so it stays a plain override, not a route.
Helper view builders shared between routes
Methods returning some View that aren't full screens — header rows, loading placeholders, anything you call from inside another route's body. Ignored.
Coordinator factories you call manually
If you build a child coordinator imperatively without going through a destination case, mark its factory as @ScaffoldingIgnored so it doesn't become an unused enum case.

Counter-example — what the macro tracks by default

Without @ScaffoldingIgnored, every function whose return type is on the auto-tracked list becomes a case in the generated Destinations enum:

func home() -> some View
.home case, view destination.
func detail(item: Item) -> some View
.detail(item:) case with the matching associated value.
func settings() -> any Coordinatable
.settings case, child-coordinator destination.
func feed() -> (any Coordinatable, some View)
.feed tab case (coordinator + label tuple).

You only need @ScaffoldingIgnored when one of these shapes shows up where you don't want it generated.

Declaration

Scaffolding.swift
@attached(peer)
public macro ScaffoldingIgnored()
    = #externalMacro(module: "ScaffoldingMacros", type: "ScaffoldingIgnoredMacro")