API Reference
Symbols.
Every public protocol, type, and macro the Scaffolding module exposes — declaration, conformance,
and member topics for each.
- Protocols · 4
- Containers · 3
- Destinations · 3
- Macros · 3
Coordinatable
ProtocolThe 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
@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
nilat 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 totrue; opt out with@Scaffoldable(injectsCoordinator: false).
FlowCoordinatable
ProtocolThe push/pop navigation coordinator. Wraps a SwiftUI NavigationStack and stores both pushed screens and
modal-presented coordinators in a single FlowStack.destinations array.
Declaration
@MainActor
public protocol FlowCoordinatable: Coordinatable
where ViewType == FlowCoordinatableView {
var stack: FlowStack<Self> { get }
}Conforms to
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
.sheetor.fullScreenCover. Both end up instack.destinationswith the matchingpushType. 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
ProtocolManages 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
@MainActor
public protocol TabCoordinatable: Coordinatable
where ViewType == TabCoordinatableView {
var tabItems: TabItems<Self> { get }
}Conforms to
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·.hiddenpresent(_:as:onDismiss:)- Present a modal directly on the tab coordinator — no need to delegate to a child flow.
RootCoordinatable
ProtocolHolds 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
@MainActor
public protocol RootCoordinatable: Coordinatable
where ViewType == RootCoordinatableView {
var root: Root<Self> { get }
}Conforms to
Topics
root: Root<Self>- Observable container with the current root + presented modals.
setRoot(_:animation:) -> Self- Atomic root swap. Fires
onDismisson 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>
ClassBacks 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
@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
Topics
root: Destination?- The flow's root destination.
destinations: [Destination]- Mixed array of pushes and modals (filtered by
pushTypeat 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>
ClassBacks a RootCoordinatable. modals holds
modals presented directly from the root coordinator (added via present(_:as:) on the root).
Declaration
@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
TabItems<Coordinator>
ClassBacks a TabCoordinatable. modals holds
modals presented directly from the tab coordinator.
Declaration
@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
Destination
StructA 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
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.Metacase.
Destination type enums
EnumsThree small enums classify destinations from different angles — their lifecycle origin, the internal presentation tag, and the public API surface for modal presentation.
Declaration
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 fromDestination.routeType. PresentationType- Internal tag:
.push·.sheet·.fullScreenCover. Stored onDestination.pushType. ModalPresentationType- Public modal API:
.sheet·.fullScreenCover. Pass topresent(_: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
ProtocolsDestinationable 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
@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
MacroApply 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
@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-> LoginCoordinatorare 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. Passfalseon reusable views that shouldn't bind to a specific flow.
Generates
Destinations enum (conforms to Destinationable) · _injectsCoordinator property
@ScaffoldingIgnored
MacroExcludes 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@ScaffoldingIgnoredso it stays a plain override, not a route. - Helper view builders shared between routes
- Methods returning
some Viewthat 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
@ScaffoldingIgnoredso 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- →
.homecase, view destination. func detail(item: Item) -> some View- →
.detail(item:)case with the matching associated value. func settings() -> any Coordinatable- →
.settingscase, child-coordinator destination. func feed() -> (any Coordinatable, some View)- →
.feedtab case (coordinator + label tuple).
Declaration
@attached(peer)
public macro ScaffoldingIgnored()
= #externalMacro(module: "ScaffoldingMacros", type: "ScaffoldingIgnoredMacro")