KahWee

Thoughts on web development, programming, and technology

SwiftUI's Toolbar placement API

I'm exploring SwiftUI as a beginner iOS developer, and the toolbar API demonstrates something both impressive and frustrating: the framework decides what to show and what to hide based on context you didn't necessarily ask it to consider.

You describe placement intent—"I want this button in the top-right corner"—and SwiftUI interprets based on device, orientation, and available space. Sometimes it honors your intent. Sometimes it moves items to overflow menus. Sometimes it hides them entirely.

This post covers:

  • How to use ToolbarItem (the basics)
  • The four corners pattern with actual behavior
  • Why this automatic behavior frustrates simple use cases
  • What happens with too many toolbar items
  • When this approach helps vs when it hurts

ToolbarItem Basics

SwiftUI's toolbar API uses a toolbar modifier that takes ToolbarItem elements. Each ToolbarItem needs placement (where you want it) and content (what to show). Apple's ToolbarItemPlacement documentation lists placements like .topBarLeading, .topBarTrailing, .bottomBar, .status, .title, and .subtitle.

Here's the simplest example:

import SwiftUI

struct BasicToolbarDemo: View {
    var body: some View {
        NavigationStack {
            Text("Content goes here")
                .navigationTitle("Basic Toolbar")
                .toolbar {
                    // Single toolbar item in top-right
                    ToolbarItem(placement: .topBarTrailing) {
                        Button { } label: {
                            Image(systemName: "plus")
                        }
                    }
                }
        }
    }
}

Key points:

  • Must be inside NavigationStack - Toolbars attach to navigation bars
  • placement parameter - Tells SwiftUI where you want it
  • Content closure - What to display (usually a Button)

Common placements:

  • .topBarLeading - Top-left corner (menu, back button)
  • .topBarTrailing - Top-right corner (add, edit, share)
  • .bottomBar - Bottom toolbar area
  • .status - Centered in bottom bar (non-interactive status text)
  • .title - Custom title view in navigation bar
  • .subtitle - Subtitle text below title

Now let's see what happens when you use multiple placements.

The Four Corners Pattern

I wanted to place toolbar items in all four screen corners. This is common in file browsers (Files app), editing apps (Pages, TextEdit), and photo apps (Photos). Here's the simplest version:

struct FourCornersDemo: View {
    var body: some View {
        NavigationStack {
            // DemoData.generatePeople() creates 50 placeholder names
            List(DemoData.generatePeople(), id: \.self) { person in
                Text(person)
            }
            .navigationTitle("Four Corners")
            .toolbar {
                ToolbarItem(placement: .topBarLeading) {
                    Button { } label: {
                        Image(systemName: "line.3.horizontal")
                    }
                }

                ToolbarItem(placement: .topBarTrailing) {
                    Button { } label: {
                        Image(systemName: "plus")
                    }
                }

                ToolbarItem(placement: .bottomBar) {
                    Button { } label: {
                        Label("Back", systemImage: "chevron.backward")
                    }
                }

                ToolbarItem(placement: .bottomBar) {
                    Spacer()
                }

                ToolbarItem(placement: .bottomBar) {
                    Button { } label: {
                        Label("Next", systemImage: "chevron.forward")
                    }
                }
            }
        }
    }
}

Five ToolbarItems: top-left, top-right, bottom-left, a Spacer in the middle, and bottom-right. The Spacer pushes the bottom buttons to the edges.

On iPhone, all four buttons appear exactly where specified. On iPad, same buttons with more spacing. On macOS, the top buttons appear in the window toolbar, but bottom buttons disappear—macOS windows don't have bottom toolbars, so SwiftUI hides them.

More Toolbar Placements: Kitchen Sink

SwiftUI actually has more toolbar placements beyond the four corners. Here's a kitchen sink example showing .title, .subtitle, and .status:

struct ToolbarKitchenSink: View {
    @State private var selectedCount = 3
    private let people = DemoData.generatePeople()

    var body: some View {
        NavigationStack {
            List(people, id: \.self) { person in
                Text(person)
            }
            .navigationTitle("Kitchen Sink")
            .toolbar {
                ToolbarItem(placement: .topBarLeading) {
                    Button { } label: {
                        Image(systemName: "line.3.horizontal")
                    }
                }

                ToolbarItem(placement: .topBarTrailing) {
                    Button { } label: {
                        Image(systemName: "plus")
                    }
                }

                ToolbarItem(placement: .title) {
                    HStack(spacing: 6) {
                        Image(systemName: "tray.fill")
                        Text("Inbox")
                    }
                }

                ToolbarItem(placement: .subtitle) {
                    Text("\(selectedCount) of \(people.count) people selected")
                        .foregroundStyle(.secondary)
                }

                ToolbarItem(placement: .status) {
                    Button { } label: {
                        Text("\(selectedCount) of \(people.count) people selected")
                    }
                    .controlSize(.large)
                }

                ToolbarItem(placement: .bottomBar) {
                    Button { } label: {
                        Label("Back", systemImage: "chevron.backward")
                    }
                }

                ToolbarItem(placement: .bottomBar) {
                    Button { } label: {
                        Label("Next", systemImage: "chevron.forward")
                    }
                }
            }
        }
    }
}

This demonstrates:

  • .title - Custom title view in navigation bar (replaces standard title)
  • .subtitle - Subtitle text below title
  • .status - Centered in bottom bar (can be button or text)

The Frustration: Portrait-Only iPhone Apps

Here's my issue: if you're building a portrait-only iPhone app, SwiftUI's automatic cross-platform adaptation is solving a problem you don't have.

I want to build simple iPhone apps with fixed button placement. But SwiftUI's toolbar API assumes universal apps that might run on iPad or macOS, rotate to landscape, and need adaptive layouts.

Apple's documentation doesn't make the constraints clear upfront. You discover them by testing. The docs say:

"In compact horizontal size classes, the system limits both the leading and trailing positions of the navigation bar to a single item each."

Translation: iPhone portrait gets one button in .topBarLeading and one button in .topBarTrailing. Want more? They go to overflow menus automatically.

Too Many Toolbar Items: Automatic Overflow

What happens when you add 10 toolbar buttons? SwiftUI automatically creates an overflow menu. I wanted to see this in action, so I created a demo with a star button (left) and 10 action icons (right):

struct TenItemsTopTrailing: View {
    @State private var isStarred = false
    @State private var showAlert = false
    @State private var alertMessage = ""

    private let trailingIcons = [
        "pencil", "trash", "square.and.arrow.up", "doc.on.doc",
        "folder", "tag", "link", "clock", "bell", "flag"
    ]

    var body: some View {
        NavigationStack {
            List(DemoData.generatePeople(), id: \.self) { person in
                Text(person)
            }
            .navigationTitle("Lots of Actions")
            .alert(alertMessage, isPresented: $showAlert) {
                Button("OK") { }
            }
            .toolbar {
                ToolbarItem(placement: .topBarLeading) {
                    Button {
                        isStarred.toggle()
                        alertMessage = isStarred ? "Starred" : "Unstarred"
                        showAlert = true
                    } label: {
                        Image(systemName: isStarred ? "star.fill" : "star")
                            .foregroundColor(isStarred ? .yellow : .primary)
                    }
                }

                ToolbarItemGroup(placement: .topBarTrailing) {
                    ForEach(trailingIcons, id: \.self) { icon in
                        Button {
                            alertMessage = "\(icon.capitalized) clicked"
                            showAlert = true
                        } label: {
                            Image(systemName: icon)
                        }
                    }
                }
            }
        }
    }
}

The overflow menu appears automatically. I didn't write code to create it. SwiftUI saw 10 items, measured available space, and moved the extras to an overflow menu. But here's the frustrating part: SwiftUI doesn't give you any control over this overflow behavior. No API to customize it, no way to prioritize which items stay visible, no option to use a different overflow UI. The API feels incomplete—it just happens to you.

On iPhone, you see 4-5 icons plus a "•••" overflow button. On iPad, 5-7 icons before overflow. On macOS, all 10 might fit if the window is wide enough. Which items get hidden? SwiftUI decides. Want to keep the most important actions visible? Too bad. SwiftUI picks based on its own logic.

If you're building a simple iPhone app and wanted all 10 icons visible in a compact layout, you can't. SwiftUI decides based on Human Interface Guidelines, and you get no say in the matter.

What Apple Wants vs What Developers Often Need

Apple wants:

  • Universal apps that run on iPhone, iPad, macOS, visionOS
  • Consistent adherence to Human Interface Guidelines
  • Automatic adaptation to different contexts
  • Apps that feel native on each platform

Many developers want:

  • Simple iPhone portrait app
  • Exact control over button placement
  • Predictable behavior (button always appears here)
  • No hidden overflow menus

These goals conflict. SwiftUI's toolbar API optimizes for Apple's vision—universal apps that adapt everywhere. That's impressive engineering for cross-platform development. But for simple iPhone apps, it creates friction. The framework makes decisions for you, and you can't override them.

All Tags