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
- Building the iPad Menu Bar with Commands
- Window Controls and Toolbar Adaptation
- Advanced Usage
- Performance Considerations
- When to Use (and When Not To)
- Summary
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
.commandsfor 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 (likeNSPasteboard) 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.pliststill setsUIRequiresFullscreentotrue, 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
#availablecheck 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 Bodyinvocations that spike when the menu opens.
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Document-based or productivity app | Adopt the full menu bar. Users expect File, Edit, and View menus. |
App already shipping .commands for macOS | You are done. iPadOS 26 picks up your existing commands automatically. |
| Simple single-screen app | Skip custom menus. The system provides default Edit and View menus. |
| Game or immersive media app | Minimal menu bar. A “Game” menu for save/load/settings suffices. |
App with UIRequiresFullscreen | Remove the key. It is deprecated and blocks window controls. |
| Custom close/minimize buttons | Gate 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
.commandsmodifier now renders a native, swipe-down menu bar on iPadOS 26. If your app already declaresCommandMenuorCommandGroupfor macOS, it works on iPad with zero changes. CommandMenucreates custom top-level menus;CommandGroupinserts items into existing system menus.- Window controls (close, minimize, arrange) appear automatically on the leading edge of the toolbar. Use
containerCornerOffsetfor custom layouts that need to avoid occlusion. windowResizeAnchorcontrols where the window grows from during live resizing, preserving visual continuity in tab-based or settings windows.- Remove
UIRequiresFullscreenfrom yourInfo.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