iPad Menu Bar and Window Controls: Productivity Features in iPadOS 26


If you have been shipping a macOS Catalyst or SwiftUI multiplatform app, you already have a .commands modifier attached to your WindowGroup. In iPadOS 26 that same code now renders a fully native menu bar on iPad — no extra work required. Pair that with the new close, minimize, and arrange window controls that land on the leading edge of every toolbar, and the iPad finally has a first-class desktop-grade windowing story.

This post covers how to build and customize the iPad menu bar with SwiftUI’s commands API, how to handle the new window controls in your toolbar layout, and how to use windowResizeAnchor to keep your UI stable during live resizing. We will not cover UIKit-specific scene lifecycle management or the Files app’s new windowing behavior — those deserve their own treatment.

Contents

The Problem

Before iPadOS 26, keyboard-attached iPads exposed your CommandMenu items only through the discoverability HUD — the overlay that appears when a user holds the Command key. There was no persistent, visible menu bar. Users had to memorize shortcuts or fall back to toolbar buttons.

Consider a document-based app that manages a library of Pixar film scripts. Your macOS version already had a rich menu bar for file operations, editing, and view options:

@main
struct PixarScriptEditorApp: App {
    var body: some Scene {
        WindowGroup {
            ScriptLibraryView()
        }
        .commands {
            CommandMenu("Scripts") {
                Button("New Script") {
                    ScriptManager.shared.createScript()
                }
                .keyboardShortcut("n")

                Button("Import from Storyboard...") {
                    ScriptManager.shared.importFromStoryboard()
                }
                .keyboardShortcut("i", modifiers: [.command, .shift])

                Divider()

                Button("Export as PDF") {
                    ScriptManager.shared.exportPDF()
                }
                .keyboardShortcut("e", modifiers: [.command, .shift])
            }
        }
    }
}

On macOS, users saw a “Scripts” menu in the menu bar. On iPad before iPadOS 26, those items were invisible unless the user happened to hold the Command key. That discoverability gap meant power features went unused, and developers often duplicated commands in toolbars and context menus just to surface them on iPad.

iPadOS 26 closes this gap entirely. The same .commands modifier now creates a swipe-down menu bar on iPad, identical in structure to its macOS counterpart.

Building the iPad Menu Bar with Commands

The menu bar on iPad is activated when the user swipes down from the top of the app window (or moves the pointer to the top edge). It displays every CommandMenu and CommandGroup you have declared, in declaration order, inserted between the system-provided menus.

CommandMenu: Custom Top-Level Menus

CommandMenu creates a standalone, top-level menu. Use it for domain-specific actions that do not fit into the built-in system menus.

.commands {
    CommandMenu("Characters") {
        Button("Add Character") {
            viewModel.addCharacter()
        }
        .keyboardShortcut("n", modifiers: [.command, .option])

        Button("Duplicate Character") {
            viewModel.duplicateSelectedCharacter()
        }
        .keyboardShortcut("d")

        Divider()

        Menu("Assign to Film") {
            ForEach(viewModel.films) { film in
                Button(film.title) {
                    viewModel.assignCharacter(to: film)
                }
            }
        }
    }
}

On iPad, the “Characters” menu appears between the built-in View and Window menus. Keyboard shortcuts render inline in the menu items, exactly as they do on macOS.

CommandGroup: Extending System Menus

CommandGroup lets you insert items before, after, or in place of existing system menu groups. This is the right tool when your action belongs alongside standard commands.

.commands {
    // Add a "Storyboard" item after the standard New Window group
    CommandGroup(after: .newItem) {
        Button("New Storyboard") {
            StoryboardManager.shared.create()
        }
        .keyboardShortcut("b", modifiers: [.command, .shift])
    }

    // Replace the default paste commands with a richer set
    CommandGroup(replacing: .pasteboard) {
        Button("Paste") {
            ClipboardService.paste()
        }
        .keyboardShortcut("v")

        Button("Paste as Plain Text") {
            ClipboardService.pastePlainText()
        }
        .keyboardShortcut("v", modifiers: [.command, .shift])
    }
}

CommandGroupPlacement defines the insertion points. The most commonly used placements include .newItem, .pasteboard, .textEditing, .toolbar, .sidebar, and .help.

Built-In Command Sets

SwiftUI ships a handful of pre-built command conformances that you can drop in directly:

.commands {
    TextEditingCommands()   // Undo, redo, cut, copy, paste
    TextFormattingCommands() // Bold, italic, underline
    ToolbarCommands()       // Show/hide toolbar, customize toolbar
    SidebarCommands()       // Toggle sidebar visibility
}

These are especially useful for document-based apps. On iPad, they populate the menu bar with standard editing and view commands that users expect.

Tip: If your app already uses .commands for macOS, audit your existing menus for iPad suitability. Menu items that trigger popovers or sheets work fine, but items that rely on macOS-only APIs (like NSPasteboard) will need platform-conditional compilation.

Window Controls and Toolbar Adaptation

iPadOS 26 introduces close, minimize, and arrange controls that appear on the leading edge of the toolbar — visually similar to the macOS traffic-light buttons. These controls are system-managed; you do not create them. However, their presence shifts your existing toolbar content to the trailing side, and you need to handle that gracefully.

How Window Controls Behave

When a user taps the window controls region, the buttons expand to reveal their actions:

  • Close dismisses the window (or the scene, for multi-window apps).
  • Minimize collapses the window to the shelf.
  • Arrange presents a layout picker for split-screen and tiling arrangements.

Press-and-hold expands the controls further, showing shortcuts for common window arrangements. This is all system behavior — you get it for free if your app supports multiple windows.

Note: If your Info.plist still sets UIRequiresFullscreen to true, the window controls will not appear. This key is deprecated in iPadOS 26. Remove it to opt into the new windowing experience.

Adapting Your Toolbar Layout

If you use NavigationStack or NavigationSplitView with standard .toolbar modifiers, the system automatically shifts your items to accommodate the window controls. No code changes are needed.

For custom layouts that position elements at the leading edge of the toolbar, iPadOS 26 introduces the containerCornerOffset modifier to prevent occlusion:

struct ScriptEditorToolbar: View {
    var body: some View {
        HStack {
            MovieTitleLabel()
            Spacer()
            ShareButton()
            ExportButton()
        }
        .containerCornerOffset(.topLeading, sizeToFit: true)
    }
}

When sizeToFit is true, SwiftUI subtracts the width of the window controls from the available layout space, preventing your content from sliding underneath them. When false, the modifier applies only the offset without resizing.

Warning: If you have placed custom close or minimize buttons in your toolbar (a common pattern for pre-iPadOS 26 multi-window apps), you will end up with duplicate controls. Wrap those in an #available check and remove them on iPadOS 26.

.toolbar {
    #if !os(macOS)
    if #unavailable(iOS 26) {
        ToolbarItem(placement: .navigationBarLeading) {
            Button("Close", systemImage: "xmark") {
                dismiss()
            }
        }
    }
    #endif
}

Advanced Usage

Controlling Window Resize Behavior with windowResizeAnchor

When users drag to resize a window on iPad (a new iPadOS 26 gesture), the content animates to fill the new frame. By default, the resize anchors from the center. windowResizeAnchor lets you change that anchor point so content resizes from a more natural origin.

struct SettingsWindow: Scene {
    var body: some Scene {
        Window("Settings", id: "settings") {
            SettingsView()
        }
        .windowResizeAnchor(.topLeading)
        .windowResizability(.contentSize)
    }
}

This is particularly useful for tab-based interfaces where the selected tab determines the window size. Anchoring to .topLeading keeps the tab bar pinned while the content area grows or shrinks below it:

struct FilmDatabaseApp: App {
    @State private var selectedTab: SettingsTab = .general

    var body: some Scene {
        Window("Film Database Settings", id: "settings") {
            TabView(selection: $selectedTab) {
                GeneralSettingsView()
                    .tag(SettingsTab.general)
                    .tabItem { Label("General", systemImage: "gear") }

                RenderSettingsView()
                    .tag(SettingsTab.rendering)
                    .tabItem { Label("Rendering", systemImage: "film") }

                PluginSettingsView()
                    .tag(SettingsTab.plugins)
                    .tabItem { Label("Plugins", systemImage: "puzzlepiece") }
            }
        }
        .windowResizeAnchor(.topLeading)
        .windowResizability(.contentSize)
    }
}

The anchor preserves visual continuity — when switching from a compact “General” tab to a taller “Plugins” tab, the window grows downward rather than expanding in all directions.

Dynamic Command State

Menu items are live SwiftUI views. You can bind them to state, disable them conditionally, and reflect selection:

CommandMenu("Scene") {
    let scenes = viewModel.availableScenes

    ForEach(scenes) { scene in
        Button(scene.name) {
            viewModel.selectScene(scene)
        }
        .disabled(!scene.isReady)
    }

    Divider()

    Toggle(
        "Show Storyboard Annotations",
        isOn: $viewModel.showAnnotations
    )
    .keyboardShortcut("a", modifiers: [.command, .option])
}

The Toggle renders as a checkmark-toggled menu item in the menu bar, consistent with macOS behavior. ForEach dynamically populates the menu, so it updates whenever availableScenes changes.

Multi-Window Scene Commands

For apps that manage multiple window types — say, a main library window and separate editor windows — you can scope commands to specific scenes:

@main
struct PixarStudioApp: App {
    var body: some Scene {
        WindowGroup("Library", id: "library") {
            LibraryView()
        }
        .commands {
            CommandMenu("Library") {
                Button("Refresh Catalog") {
                    LibraryCatalog.shared.refresh()
                }
                .keyboardShortcut("r")
            }
        }

        WindowGroup("Script Editor", id: "editor",
                     for: Script.ID.self) { $scriptID in
            ScriptEditorView(scriptID: scriptID)
        }
        .commands {
            CommandMenu("Editing") {
                Button("Run Spell Check") {
                    SpellChecker.shared.check()
                }
                .keyboardShortcut(":")
            }
        }
    }
}

Each scene contributes its own commands to the menu bar. When the user focuses the Library window, both the “Library” and “Editing” menus appear. SwiftUI merges them automatically.

Apple Docs: Commands — SwiftUI Framework

Performance Considerations

The menu bar itself has negligible performance cost — it is constructed lazily when the user invokes it and torn down when dismissed. However, a few patterns can introduce unnecessary overhead:

Avoid expensive computation in menu body closures. The CommandMenu view builder is evaluated every time the menu opens. If you are fetching data or running filters inside the closure, extract that work into a published property on your view model:

// Avoid: filtering inside the menu body
CommandMenu("Characters") {
    ForEach(store.characters.filter {
        $0.film == selectedFilm
    }) { character in
        Button(character.name) { select(character) }
    }
}

// Prefer: pre-computed published property
CommandMenu("Characters") {
    ForEach(viewModel.filteredCharacters) { character in
        Button(character.name) { select(character) }
    }
}

Keep menu hierarchies shallow. Deeply nested Menu items inside CommandMenu add visual complexity and increase the view diff cost. Two levels of nesting is a reasonable maximum — beyond that, consider using a dedicated window or popover.

Window resize performance. The windowResizeAnchor modifier itself is free, but the content you display during a live resize is not. If your view hierarchy includes expensive layout passes (large LazyVGrid reflows, for instance), consider using ContentSizeCategory and GeometryReader to simplify your layout at small sizes rather than re-rendering the full complex layout on every frame.

Tip: Use the SwiftUI Instruments template (introduced in Xcode 16) to profile menu bar construction time and window resize frame drops. Look for View Body invocations that spike when the menu opens.

When to Use (and When Not To)

ScenarioRecommendation
Document-based or productivity appAdopt the full menu bar. Users expect File, Edit, and View menus.
App already shipping .commands for macOSYou are done. iPadOS 26 picks up your existing commands automatically.
Simple single-screen appSkip custom menus. The system provides default Edit and View menus.
Game or immersive media appMinimal menu bar. A “Game” menu for save/load/settings suffices.
App with UIRequiresFullscreenRemove the key. It is deprecated and blocks window controls.
Custom close/minimize buttonsGate them with #unavailable(iOS 26) to avoid duplication.

The general principle: if your app benefits from keyboard shortcuts and power-user discoverability, invest in the menu bar. If your app is primarily touch-first with no document or editing model, the default system menus are sufficient.

Summary

  • The .commands modifier now renders a native, swipe-down menu bar on iPadOS 26. If your app already declares CommandMenu or CommandGroup for macOS, it works on iPad with zero changes.
  • CommandMenu creates custom top-level menus; CommandGroup inserts items into existing system menus.
  • Window controls (close, minimize, arrange) appear automatically on the leading edge of the toolbar. Use containerCornerOffset for custom layouts that need to avoid occlusion.
  • windowResizeAnchor controls where the window grows from during live resizing, preserving visual continuity in tab-based or settings windows.
  • Remove UIRequiresFullscreen from your Info.plist — it is deprecated and blocks the entire windowing experience.

For a deeper look at how the navigation stack, tab bar, and sidebar have changed in iPadOS 26, see SwiftUI Navigation Migration for iOS 26. If you need to customize toolbar spacers, badges, and scroll-edge blur alongside your new menu bar, check out SwiftUI Toolbars in iOS 26.

WWDC Sessions: What’s new in SwiftUI — WWDC25 Session 256 Elevate the design of your iPad app — WWDC25 Session 208