- Published on
- 7 min read Intermediate
> What's New in SwiftUI for iOS 27
The SwiftUI story at WWDC 2026 has a clear theme: features that used to be exclusive to List now work everywhere. Drag-to-reorder and swipe actions both escaped their container this year, AsyncImage finally caches, and alerts picked up the presentation pattern sheets have had since iOS 14. There's also one change under the hood, to @State itself, that can break existing code when you rebuild with the new SDK.
Here's what stood out to me after a couple of days with the iOS 27 SDK in Xcode 27. All of it applies to macOS 27, watchOS 27, and visionOS 27 too unless I note otherwise.
Reorder Anything
Drag-to-reorder used to mean List and onMove(perform:), or a pile of custom drag gesture code. In iOS 27 any container can do it: grids, stacks, even custom layouts. Two modifiers split the job. reorderable() goes on the ForEach, and reorderContainer(for:) goes on the enclosing container with a closure that applies the change to your data.
struct DeckGridView: View {
@State private var cards: [Flashcard]
let columns = [GridItem(.adaptive(minimum: 120))]
var body: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(cards) { card in
FlashcardView(card: card)
}
.reorderable()
}
.reorderContainer(for: Flashcard.self) { difference in
apply(difference)
}
}
}
}
Instead of source and destination indices, the closure hands you a ReorderDifference. Its sources property holds the IDs of the items being moved, and destination.position tells you where they land, either .before(someID) or .end. Applying it looks like this:
extension DeckGridView {
func apply(_ difference: ReorderDifference<Flashcard.ID, ReorderableSingleCollectionIdentifier>) {
var moved: [Flashcard] = []
cards.removeAll { card in
guard difference.sources.contains(card.id) else { return false }
moved.append(card)
return true
}
switch difference.destination.position {
case .before(let id):
let index = cards.firstIndex { $0.id == id } ?? cards.endIndex
cards.insert(contentsOf: moved, at: index)
case .end:
cards.append(contentsOf: moved)
}
}
}
Sectioned data works too: tag each ForEach with reorderable(collectionID:) and use the reorderContainer(for:in:) overload, and the difference tells you which section the drop landed in. There's also a dropDestination(for:isEnabled:) overload for combining items by dropping one onto another, which the system visualizes for you. The whole family is available everywhere except tvOS, which makes sense given there's nothing to drag with.
Swipe Actions Outside of List
The companion change: swipeActions now works in any scrollable container. The row modifier itself is unchanged since iOS 15. What's new is swipeActionsContainer(), which you put on the ScrollView so it can coordinate swipes across the rows inside it.
ScrollView {
LazyVStack {
ForEach(cards) { card in
FlashcardRow(card: card)
.swipeActions {
Button(role: .destructive) {
cards.removeAll { $0.id == card.id }
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
}
.swipeActionsContainer()
Without the container modifier, swipeActions on a row outside a List silently does nothing, so if your swipes aren't appearing, that's the first thing to check. A new onPresentationChanged closure on the row modifier tells you when a row's actions are revealed or hidden, which is handy for closing other rows or pausing content underneath.
If you've ever rebuilt a screen as a ScrollView plus LazyVStack purely for layout control and then mourned the loss of swipe-to-delete, this is the release that fixes it.
AsyncImage Caches Now
AsyncImage has been easy to demo and hard to ship, mostly because it didn't cache. That changes in iOS 27, and in the best possible way: standard HTTP caching is on by default, honoring the server's cache headers, with no code change required. It's runtime behavior rather than an API, so even apps built against older SDKs pick it up when running on iOS 27.
When you want control, the new init(request:) initializers accept a URLRequest in place of a URL:
AsyncImage(request: URLRequest(url: photo.url, cachePolicy: .returnCacheDataElseLoad)) { image in
image.resizable().scaledToFit()
} placeholder: {
ProgressView()
}
And the asyncImageURLSession(_:) modifier lets a whole subtree share a custom URLSession, which is where you size the cache:
private static let imageSession: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.urlCache = URLCache(
memoryCapacity: 64 * 1024 * 1024,
diskCapacity: 256 * 1024 * 1024
)
return URLSession(configuration: configuration)
}()
This won't replace Kingfisher or Nuke for apps that need downsampling or processing pipelines, but for the common case of "show remote images without re-downloading them every scroll," AsyncImage is finally enough on its own.
Alerts and Dialogs Take an Item
sheet(item:) has always been the nicest presentation API in SwiftUI: one optional drives the presentation and delivers the value. In iOS 27, alert and confirmationDialog gain the same shape. The dialog presents while the binding is non-nil, your builders receive the unwrapped value, and SwiftUI nils the binding on dismiss.
struct DeckListView: View {
@State private var deckToDelete: Deck?
var body: some View {
DeckList(deleteAction: { deckToDelete = $0 })
.confirmationDialog("Delete deck?", item: $deckToDelete) { deck in
Button("Delete \(deck.name)", role: .destructive) {
delete(deck)
}
} message: { deck in
Text("\(deck.name) and its cards will be removed from all of your devices.")
}
}
}
No more pairing a Bool with a separately stored optional, and no more presenting: argument. The item type doesn't even need to be Identifiable. Small change, but it deletes a familiar chunk of boilerplate from almost every app.
Toolbars Learn to Overflow Gracefully
When a toolbar runs out of room, the system now moves items into a trailing overflow menu, and iOS 27 gives you a say in what goes where. visibilityPriority(_:) marks the items that should stay in the bar the longest, ToolbarOverflowMenu holds actions that should always live in the menu, and .topBarPinnedTrailing pins one item that never overflows.
.toolbar {
ToolbarItemGroup {
UndoButton()
RedoButton()
}
.visibilityPriority(.high)
ToolbarItem(placement: .topBarPinnedTrailing) {
ShareButton()
}
ToolbarOverflowMenu {
ExportButton()
ResetProgressButton()
}
}
Two more toolbar changes worth knowing. toolbarMinimizeBehavior(.onScrollDown, for: .navigationBar) shrinks the bar as the user scrolls, and the status bar is now a ToolbarPlacement, so toolbarVisibility(.hidden, for: .statusBar) replaces statusBarHidden(_:) on iOS. ForEach also works inside toolbar builders now, and that one back-deploys to iOS 16 as long as you build with the new SDK.
@State Is a Macro Now
The change most likely to break your build isn't a new feature at all. @State migrated from a property wrapper to a macro in the iOS 27 SDK, and code that compiled fine before can fail after the update. The most common error looks like this:
error: Variable 'self.name' used before being initialized
It shows up when a view gives @State an initial value at the declaration and then also assigns it in init before other stored properties. The tempting fix is to reorder the assignments. Don't. The correct fix is to drop the initial value from the declaration and assign only in the initializer. Reordering can get it compiling while leaving the old anti-pattern in place, where the assigned value silently loses to the declaration's initial value at runtime.
While you're rebuilding, watch for two related source breaks: composing another property wrapper with @State now collides on the synthesized backing storage, and the unified @ContentBuilder result builder can surface ambiguous overload errors in overlay and background closures that previously relied on a specific builder structure. Both are rare, and both fail loudly at compile time rather than misbehaving at runtime.
The Smaller Stuff
A few more items from the release notes that didn't fit above. Document-based apps get a new ReadableDocument and WritableDocument API that supports read-only viewers and background reads and writes, replacing FileDocument for new code targeting iOS 27. EmptyView is valid toolbar content for explicit empty branches. And on visionOS, statusBarHidden(_:) is hard-deprecated because it never did anything there; delete the call.
None of this requires raising your deployment target on day one. Everything above gates cleanly behind if #available(iOS 27, *), and the two biggest wins, AsyncImage caching for existing code and the @State migration, come along the moment you rebuild. Just budget a regression pass for the rebuild itself.
Sample Project
Want to see this code in action? Check out the complete sample project on GitHub:
The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.
// Continue_Learning
WWDC 2026 Predictions Report Card: What Apple Actually Shipped
Before WWDC 2026 I wrote down predictions and never hit publish. Four months later, with iOS 27 out and iPhone Duo days away, it's time to grade them against what Apple actually shipped.
iPhone Duo Split View: Is Your Window Left or Right?
When two windows share iPhone Duo's inner display, neither one is told which side it's on. I built a proof of concept in Xcode 27.1 and found the fold's ReservedRegion answers it, once you know its frame is clipped to your window.
iPhone Duo Teleprompter with CameraCaptureAccessory
Use CameraCaptureAccessory to show a scrolling teleprompter on iPhone Duo's outer display while the rear camera records, in SwiftUI and UIKit.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.