Skip to content

Navigation

Daniel S edited this page Jul 2, 2026 · 1 revision

Navigation

DesignFoundation provides three navigation components — DFSidebar, DFTabBar, and the dfNavigationBar view modifier — that work identically across iOS, macOS, and visionOS without #if os() guards. Platform differences (split-view vs. tab bar, title bar vs. inline header) are handled internally through DFPlatformContext.

Related pages: Getting-Started · Theming · Cross-Platform · Layout


DFSidebar

DFSidebar is the primary hierarchical navigation component for multi-section interfaces. On macOS it renders as a native sidebar; on iPad in regular width it renders as a split-view sidebar; on iPhone it collapses to a sheet or drawer pattern — all from the same call site.

Data Types

public struct DFSidebarItem: Identifiable, Sendable {
    public let id:    String
    public let icon:  String       // SF Symbol name
    public let label: String
    public var badge: DFBadgeContent?

    public enum DFBadgeContent: Sendable {
        case count(Int)       // Numeric badge: "3", "12", "99+"
        case dot              // Presence indicator dot
        case text(String)     // Custom text: "Pro", "New"
    }
}

public struct DFSidebarSection: Identifiable, Sendable {
    public let id:    String
    public let title: String       // Section header, or "" for ungrouped items
    public let items: [DFSidebarItem]
}

Initializer

public struct DFSidebar: View {
    public init(
        selection: Binding<String?>,
        sections:  [DFSidebarSection],
        @ViewBuilder detail: (String) -> Detail
    ) where Detail: View
}
Parameter Description
selection Binding to the currently selected item ID. nil means nothing selected.
sections Ordered array of sidebar sections. Sections with an empty title render without a header.
detail A view builder that receives the selected item ID and returns its detail view.

Styles

Style Token Description
DFDefaultSidebarStyle .default Adaptive platform-native rendering. Default.
DFPlainSidebarStyle .plain Minimal background; no material or vibrancy.
DFGlassSidebarStyle .glass .sidebar material with vibrancy. iOS 26+ / macOS 26+.

Apply with .dfSidebarStyle(_:).

Examples

Minimal sidebar

@State private var selection: String? = "home"

let nav = [
    DFSidebarSection(id: "main", title: "", items: [
        DFSidebarItem(id: "home",     icon: "house.fill",        label: "Home"),
        DFSidebarItem(id: "explore",  icon: "magnifyingglass",   label: "Explore"),
        DFSidebarItem(id: "library",  icon: "books.vertical.fill", label: "Library"),
    ]),
]

DFSidebar(selection: $selection, sections: nav) { id in
    switch id {
    case "home":    HomeView()
    case "explore": ExploreView()
    case "library": LibraryView()
    default:        EmptyView()
    }
}

With section headers and badges

let nav = [
    DFSidebarSection(id: "dashboards", title: "Dashboards", items: [
        DFSidebarItem(id: "overview",  icon: "chart.bar.fill",       label: "Overview"),
        DFSidebarItem(id: "analytics", icon: "chart.xyaxis.line",     label: "Analytics"),
    ]),
    DFSidebarSection(id: "workspace", title: "Workspace", items: [
        DFSidebarItem(id: "inbox",     icon: "tray.full.fill",        label: "Inbox",    badge: .count(3)),
        DFSidebarItem(id: "projects",  icon: "folder.fill",           label: "Projects", badge: .dot),
        DFSidebarItem(id: "team",      icon: "person.2.fill",         label: "Team"),
    ]),
    DFSidebarSection(id: "system", title: "System", items: [
        DFSidebarItem(id: "settings",  icon: "gearshape.fill",        label: "Settings"),
    ]),
]

DFSidebar(selection: $selection, sections: nav) { id in
    detailView(for: id)
}
.dfSidebarStyle(.glass)

Sidebar with dynamic selection

@State private var selection: String? = nil

DFSidebar(selection: $selection, sections: nav) { id in
    if let item = allItems.first(where: { $0.id == id }) {
        ItemDetailView(item: item)
    } else {
        ContentUnavailableView("Select an item", systemImage: "sidebar.left")
    }
}

Sidebar with navigation bar integration

DFSidebar(selection: $selection, sections: nav) { id in
    detailView(for: id)
        .dfNavigationBar(title: title(for: id)) {
            DFButton("New") { createNew() }
                .dfButtonStyle(.tinted)
        }
}

DFTabBar

DFTabBar is DesignFoundation's tab-based navigation container. It manages a row of labeled icon tabs and displays one content view at a time. On iPhone it renders as a bottom tab bar; on iPad regular width it can optionally promote to a sidebar; on macOS it renders as a compact tab strip.

Data Type

public struct DFTabItem: Identifiable, Sendable {
    public let id:    String
    public let icon:  String       // SF Symbol name
    public let label: String
    public var badge: DFTabBadge?

    public enum DFTabBadge: Sendable {
        case count(Int)
        case dot
    }
}

Initializer

public struct DFTabBar<Content: View>: View {
    public init(
        selection: Binding<String>,
        items:     [DFTabItem],
        @ViewBuilder content: (String) -> Content
    )
}
Parameter Description
selection Binding to the currently displayed tab ID. Must not be nil — always has a selected tab.
items Ordered array of DFTabItem values. The tab bar renders one icon+label per item.
content View builder called with the current selection ID.

Styles

Style Token Description
DFDefaultTabBarStyle .default Adaptive platform-native rendering. Default.
DFMinimalTabBarStyle .minimal Borderless, background-less tab indicator.
DFGlassTabBarStyle .glass Material-backed tab bar. iOS 26+ / macOS 26+.

Apply with .dfTabBarStyle(_:).

Examples

Minimal tab bar

@State private var tab = "home"

let tabs = [
    DFTabItem(id: "home",     icon: "house.fill",    label: "Home"),
    DFTabItem(id: "search",   icon: "magnifyingglass", label: "Search"),
    DFTabItem(id: "activity", icon: "bell.fill",      label: "Activity"),
    DFTabItem(id: "profile",  icon: "person.fill",    label: "Profile"),
]

DFTabBar(selection: $tab, items: tabs) { id in
    switch id {
    case "home":     HomeView()
    case "search":   SearchView()
    case "activity": ActivityView()
    case "profile":  ProfileView()
    default:         EmptyView()
    }
}

With badges

let tabs = [
    DFTabItem(id: "feed",     icon: "list.bullet",   label: "Feed"),
    DFTabItem(id: "messages", icon: "message.fill",  label: "Messages", badge: .count(7)),
    DFTabItem(id: "alerts",   icon: "bell.badge.fill", label: "Alerts", badge: .dot),
    DFTabItem(id: "profile",  icon: "person.fill",   label: "Profile"),
]

DFTabBar(selection: $tab, items: tabs) { id in
    tabContent(for: id)
}
.dfTabBarStyle(.glass)

Adaptive layout — sidebar on regular width, tab bar on compact

@Environment(\.horizontalSizeClass) private var hSizeClass
@State private var selection = "home"

var body: some View {
    if hSizeClass == .regular {
        DFSidebar(selection: $optionalSelection, sections: sidebarSections) { id in
            content(for: id)
        }
    } else {
        DFTabBar(selection: $selection, items: tabItems) { id in
            content(for: id)
        }
    }
}

private var optionalSelection: Binding<String?> {
    Binding(
        get: { selection },
        set: { selection = $0 ?? "home" }
    )
}

dfNavigationBar

dfNavigationBar is a view modifier — not a standalone view — that adds a themed navigation bar to any view. It handles platform differences (toolbar vs. inline title, back button placement) internally.

Overloads

// 1. Title only
func dfNavigationBar(title: String) -> some View

// 2. Title + trailing actions
func dfNavigationBar(
    title: String,
    @ViewBuilder trailing: () -> TrailingContent
) -> some View where TrailingContent: View

// 3. Title + leading + trailing actions
func dfNavigationBar(
    title: String,
    @ViewBuilder leading: () -> LeadingContent,
    @ViewBuilder trailing: () -> TrailingContent
) -> some View where LeadingContent: View, TrailingContent: View

Display Modes

Mode Token Description
.automatic Default Platform default. Large on iOS root views, inline on deeper views.
.inline .inline Compact bar with small centered title.
.large .large Prominent large title; scrolls away on scroll.

Apply with .dfNavigationBarDisplayMode(_:).

Styles

Style Token Description
Default .default Standard opaque navigation bar styled to the active theme.
Glass .glass .bar material with translucency. iOS 26+ / macOS 26+.

Apply with .dfNavigationBarStyle(_:).

Examples

Title only

ProjectListView()
    .dfNavigationBar(title: "Projects")

With trailing action button

ContactListView()
    .dfNavigationBar(title: "Contacts") {
        DFButton("Add") { showCreateContact = true }
            .dfButtonStyle(.tinted)
    }

Inline mode on a detail screen

DocumentView(document: doc)
    .dfNavigationBar(title: doc.title) {
        DFButton("Share") { shareDocument() }
            .dfButtonStyle(.ghost)
    }
    .dfNavigationBarDisplayMode(.inline)

With leading and trailing controls

EditorView()
    .dfNavigationBar(
        title: "Edit Profile",
        leading: {
            DFButton("Cancel") { cancel() }
                .dfButtonStyle(.ghost)
        },
        trailing: {
            DFButton("Save") { save() }
                .disabled(!hasChanges)
        }
    )
    .dfNavigationBarDisplayMode(.inline)

Notes on Theming

All three components inherit the active theme from @Environment(\.dfTheme). Navigation bars use theme.colors.surfaceElevated for their background, theme.colors.textPrimary for titles, and theme.colors.primary for tinted elements. Sidebar and tab bar item tints use theme.colors.primary for the selected state and theme.colors.textSecondary for unselected.

Override the navigation bar background for an individual screen by setting a modified theme on the content view:

DashboardView()
    .dfTheme({ var t = theme; t.colors.surfaceElevated = .clear; return t }())
    .dfNavigationBar(title: "Dashboard")

See also: Cross-Platform · Theming · Layout · Primitives · Getting-Started

Clone this wiki locally