SwiftUI - iPadOS 27
What's New in SwiftUI for iPadOS 27
iPadOS 27 pushes SwiftUI toward Mac-like app structure: multiple windows, document launch flows, inactive-window appearance, and toolbars that survive resizing. The platform-specific work is not a new visual flourish. It is making iPad UI behave like a workspace.
iPadOS focus
Use creation sources to offer blank, template, and imported-file starting points.
Window awarenessCustom sidebar and footer content should respond to inactive-window state.
Reorderable gridsUse the new reordering APIs for visual iPad collections without turning them into lists.
Document Launch Flows Fit iPad
The SwiftUI session introduces DocumentCreationSource and NewDocumentButton. On iPad,
this is especially useful because document apps often start from a template, an imported photo, a scan, or a
handwriting surface rather than one blank file.
The important shift is that the launch scene can describe creation choices directly. SwiftUI passes the chosen source into the document creation path, so the document can start in the right mode without a separate landing screen or a pile of temporary app state.
Fallback for iPadOS 26
Keep a normal launch view with explicit buttons. Route each button to the old document creation path and pass the chosen template through your own app state.
@main
struct NotebookApp: App {
var body: some Scene {
DocumentGroupLaunchScene("Create a Notebook") {
NewDocumentButton("Blank", source: .blank)
NewDocumentButton("From PDF", source: .pdf)
}
DocumentGroup { document in
NotebookView(document)
} newDocument: { configuration, context in
NotebookDocument(configuration: configuration, context: context)
}
}
}
Inactive Windows Are Visible State
Apple calls out a distinct inactive appearance for iPad apps, with icons and text dimming to reinforce the active window. Built-in tab and sidebar content follows the system. Custom sidebar footers, account controls, or inspector headers need the same treatment.
Use appearsActive for the pieces SwiftUI cannot infer automatically. The value is especially useful
in iPad workspace apps where two windows from the same app can sit side by side and only one is active.
struct WorkspaceFooter: View {
@Environment(\.appearsActive) private var appearsActive
var body: some View {
SyncStatusView()
.opacity(appearsActive ? 1 : 0.45)
}
}
Toolbars Need a Size Policy
iPad toolbars can move from roomy to cramped as users resize windows. Use .visibilityPriority(.high)
for editing groups, ToolbarOverflowMenu for secondary commands, and pinned trailing placement for
one action that should never disappear.
This is the same API surface as iOS, but the failure mode is different. On iPad, the toolbar may be wide in one window and narrow a moment later. Rank the commands so resize does not hide the primary edit action while leaving rarely used setup commands visible.
ToolbarItemGroup {
UndoButton()
RedoButton()
}
.visibilityPriority(.high)
ToolbarOverflowMenu {
ExportPDFButton()
ResetCanvasButton()
}
Reorderable Grids Match iPad Content
iPad apps frequently present visual collections: pages, layers, boards, photos, resources. The new reordering API works beyond lists, so you can keep the large visual grid while letting users arrange content directly.
That matters more on iPad than on iPhone because the grid often is the primary workspace. Use
.reorderable() on the generated items and .reorderContainer on the grid that owns the
move, then apply the difference to the backing collection.
LazyVGrid(columns: columns) {
ForEach(pages) { page in
PageThumbnail(page)
}
.reorderable()
}
.reorderContainer(for: Page.ID.self) { difference in
pages.apply(difference: difference)
}
Images and State Still Matter
The shared SwiftUI performance changes are relevant on iPad because workspace apps often keep multiple
columns, inspectors, thumbnails, and document previews alive. AsyncImage now respects standard
HTTP caching, and classes stored in @State are initialized lazily with the new toolchain.
Use the image change for remote thumbnails and template galleries. Use the State change to keep observable view models local to a view without paying eager initialization cost each time a parent layout refreshes.
struct TemplateTile: View {
let template: Template
@State private var model = TemplateTileModel()
var body: some View {
AsyncImage(url: template.previewURL)
.task { await model.prepare(template) }
}
}