← All daysDay 19 of 49 · Part 5: Lists and navigation
Day 19

Toolbars, menus, and search

These are the lesson notes for this day. Its build-along tutorial and flash cards have not been written yet.

Lesson 22

Toolbars, menus, and search

.toolbar adds items to the navigation bar, the bottom bar, or the keyboard. Each item has a placement, such as .topBarTrailing or .primaryAction, that describes its role so the system can put it in the right place on each platform. Menu shows a pull-down list of actions. .searchable(text:) adds a search field bound to a string. You filter your own data using that string, and can add suggestions, tokens, and scopes.

New in iOS 27: .visibilityPriority keeps the important items visible when space is tight; ToolbarOverflowMenu collects the rest; the .topBarPinnedTrailing placement pins an item in place; and .toolbarMinimizeBehavior(.onScrollDown) shrinks the bar while the user scrolls.

Exercise

Add to your list a toolbar with an Add button and a Sort menu, and a search field that filters the rows as you type.

After this lesson you should be able to answer
  1. Why do toolbar items use placements that describe a role, instead of fixed positions?
  2. How does .searchable deliver the search text to your view?
  3. In iOS 27, how do you keep a toolbar item visible when there is little space?
Resources
Write the code
Interview problem · Part 5

A deep link into a navigation stack

“A notification arrives carrying https://quest.example/trips/42/log. Write the navigation so that the app opens trip 42 and then its log — and so that a Home button anywhere returns to the root. Then tell me how you would unit test it.”

Where this comes from: Hacking with Swift: iOS interview questions · NavigationStack — from Hacking with Swift's question list: “How would you create programmatic navigation in SwiftUI?”

Show the answer
enum Route: Hashable {
    case trip(id: Int)
    case log(tripID: Int)
}

@MainActor @Observable
final class Router {
    var path: [Route] = []

    func open(_ url: URL) {                       // https://quest.example/trips/42/log
        let parts = url.pathComponents.filter { $0 != "/" }
        guard parts.first == "trips", parts.count >= 2, let id = Int(parts[1]) else { return }
        path = [.trip(id: id)]
        if parts.dropFirst(2).first == "log" { path.append(.log(tripID: id)) }
    }

    func popToRoot() { path.removeAll() }
}

struct RootView: View {
    @State private var router = Router()

    var body: some View {
        NavigationStack(path: $router.path) {
            TripList()
                .navigationDestination(for: Route.self) { route in
                    switch route {
                    case .trip(let id):     TripDetail(id: id)
                    case .log(let tripID):  TripLog(tripID: tripID)
                    }
                }
        }
        .environment(router)
        .onOpenURL { router.open($0) }
    }
}

Navigation is state. The stack on screen is a rendering of router.path. Opening a deep link is assigning an array. Returning to the root is emptying it. Restoring a session is decoding it.

The test comes for free, because the router is plain logic with no view involved: create a Router, call open(url), and assert that path == [.trip(id: 42), .log(tripID: 42)]. Then give it a malformed URL and assert that the path did not change.

What interviewers listen for: Routes that are types rather than strings, one source of truth for the stack, and the observation that turning navigation into data is what made it testable.