-
Notifications
You must be signed in to change notification settings - Fork 0
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 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.
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]
}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. |
| 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(_:).
@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()
}
}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)@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")
}
}DFSidebar(selection: $selection, sections: nav) { id in
detailView(for: id)
.dfNavigationBar(title: title(for: id)) {
DFButton("New") { createNew() }
.dfButtonStyle(.tinted)
}
}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.
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
}
}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. |
| 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(_:).
@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()
}
}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)@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 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.
// 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| 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(_:).
| 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(_:).
ProjectListView()
.dfNavigationBar(title: "Projects")ContactListView()
.dfNavigationBar(title: "Contacts") {
DFButton("Add") { showCreateContact = true }
.dfButtonStyle(.tinted)
}DocumentView(document: doc)
.dfNavigationBar(title: doc.title) {
DFButton("Share") { shareDocument() }
.dfButtonStyle(.ghost)
}
.dfNavigationBarDisplayMode(.inline)EditorView()
.dfNavigationBar(
title: "Edit Profile",
leading: {
DFButton("Cancel") { cancel() }
.dfButtonStyle(.ghost)
},
trailing: {
DFButton("Save") { save() }
.disabled(!hasChanges)
}
)
.dfNavigationBarDisplayMode(.inline)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