SwiftUI - iOS 27
What's New in SwiftUI for iOS 27
iOS 27 is the SwiftUI release where phone layouts need to behave less like fixed-size screens. Build for resizable width, rank your toolbar actions, and move list-only interactions into custom scroll layouts when the design needs it.
iOS focus
Size classes and flexible layouts beat idiom checks as iPhone apps appear in more resizable contexts.
Toolbar rankingKeep the actions people need most visible when horizontal space is tight.
Custom rowsSwipe actions and reordering no longer force a List-first design.
Resizable iPhone Layouts
Apple's SwiftUI session explicitly calls out iOS 27 iPhone app resizing and Xcode 27 resize handles in Live Previews. The code impact is straightforward: stop assuming a single compact phone canvas. Prefer layout rules driven by size, space, and content rather than device idiom.
This is still SwiftUI work even when the app looks unchanged at normal iPhone size. Views that branch on
horizontalSizeClass, available width, and content density will survive iPhone Mirroring,
iPhone-on-iPad, and preview resizing better than views that assume every iPhone screen is one fixed column.
Fallback for OS 26
Keep the same flexible layout code. Older iOS versions may not expose the same resizing behavior, but the layout still improves Split View, Dynamic Type, and iPhone-on-iPad cases.
struct LibraryLayout: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
let columns = horizontalSizeClass == .compact ? 2 : 4
ScrollView {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columns)) {
ForEach(items) { item in
LibraryTile(item)
}
}
}
}
}
Rank the Top Bar
iOS is where toolbar overflow hurts first. The OS 27 APIs let you keep undo, redo, and share where users can reach them while moving secondary commands into overflow. Use minimization only when the content area benefits from it, such as feeds, editors, galleries, and document canvases.
Treat the toolbar as a priority list, not a bag of buttons. The system can collapse low-priority items as the view narrows, but only if you mark the commands that must stay visible and move optional commands into an explicit overflow menu.
NavigationStack {
FeedView()
.toolbar {
ToolbarItemGroup {
FilterButton()
SortButton()
}
.visibilityPriority(.high)
ToolbarOverflowMenu {
ManageDownloadsButton()
ResetLayoutButton()
}
}
.toolbarMinimizeBehavior(.onScrollDown, for: .navigationBar)
}
Rows Without List Lock-In
On iPhone, custom feeds often need row gestures without List styling. In iOS 27, two separate
changes matter: swipe actions can be coordinated by a custom scroll container, and reordering can move to
stacks or grids.
Swipe Actions in Custom Scroll Views
Use this when the UI is still row-like, but the visual design is not a system List: inbox rows,
store carts, media queues, saved items, and compact task feeds.
The row keeps the familiar .swipeActions modifier. The new part is the container modifier on the
scroll view, which coordinates swipe state across all rows in that custom scroller.
Fallback for OS 26
Use List for rows that need swipe actions on older iOS releases. For a custom visual design,
provide visible trailing buttons or a context menu instead of hidden swipe gestures.
ScrollView {
LazyVStack(spacing: 8) {
ForEach(messages) { message in
MessageRow(message)
.swipeActions {
Button("Archive") { archive(message) }
}
}
}
}
.swipeActionsContainer()
Reorderable Stacks and Grids
Reordering is the better fit when the user is arranging content, not dismissing a row: photo picks, board columns, launcher tiles, dense media cards, and inspector panels.
The generated children opt into dragging with .reorderable(). The parent container receives the
difference and applies it to the source array, so model mutation stays in one place.
Fallback for OS 26
Keep List plus onMove where a list presentation is acceptable. For a custom grid,
use explicit move buttons or show the edit affordance only on iOS 27.
LazyVGrid(columns: columns) {
ForEach(cards) { card in
CardTile(card)
}
.reorderable()
}
.reorderContainer(for: Card.ID.self) { difference in
cards.apply(difference: difference)
}
Image Loading
iOS apps with feeds, stores, and image-heavy lists benefit from the AsyncImage change immediately:
it now respects HTTP caching by default. Build with Xcode 27 when you need per-request cache policy or a custom
URL cache.
Previously, scrolling back to an image could trigger another download path unless the app built its own cache
around AsyncImage. With the OS 27 behavior, server cache headers are enough for the common case.
Use the request-based initializer only when a feed needs a specific cache policy.
AsyncImage(request: URLRequest(
url: product.artworkURL,
cachePolicy: .returnCacheDataElseLoad
))
Item-Based Confirmation
Alerts and confirmation dialogs now follow the same item-binding pattern SwiftUI developers already use for sheets. On iPhone, that keeps command-driven UI small: store the selected model value, present the dialog, and let SwiftUI clear the presentation when the item goes away.
@State private var messageToDelete: Message?
var body: some View {
MessageList(messages) { message in
messageToDelete = message
}
.confirmationDialog("Delete message?", item: $messageToDelete) { message in
Button("Delete", role: .destructive) {
delete(message)
}
}
}
Lazy @State Initialization
The lazy @State initialization change is a separate win on iPhone because short-lived parent view
updates used to waste work when a child view stored an observable class in State.
This is a toolchain behavior change, not a new iOS-only visual API. If a child view declares an observable
object in @State, SwiftUI can now avoid constructing that object until the state storage is
actually used for the view lifetime.
struct FeedFilterView: View {
@State private var model = FeedFilterModel()
var body: some View {
FilterControls(selection: $model.selection)
}
}