Greetings, traveler!
If your UI was a List, you had a pretty clear path with onMove. If your UI was a grid, a stack, or a custom layout, you usually had to solve it yourself. In iOS 27, SwiftUI gets a new API for this: reorderable() and reorderContainer(for:isEnabled:move:).
The interesting part is not just that we can reorder views. We could already do that in some places. The interesting part is where the API sits. Reordering is no longer treated as a special trick owned by List. It becomes something a container can support.
The basic idea
The new API has two sides. You mark the dynamic content as reorderable:
ForEach(items) { item in
ItemRow(item: item)
}
.reorderable()Then you define a parent container where that reordering is allowed:
.reorderContainer(for: Item.self) { difference in
items.apply(difference: difference)
}reorderable() is declared on DynamicViewContent. In SwiftUI code, that usually means you apply it to a ForEach, not to an individual row view. It tells SwiftUI that the views produced by this dynamic content can participate in a reorder interaction.
reorderContainer goes on the parent container or layout view. It defines the scope of the interaction and gives you a move closure where you update your data.
So the rough mental model is:
ForEach(data) { item in
Row(item)
}
.reorderable()means “these generated views can be reordered.”
And:
.reorderContainer(for: Item.self) { difference in
// update the model
}means “this is the container where the reorder happens.”
That is a nice shape for SwiftUI. The item behavior lives with the dynamic content. The interaction boundary lives with the container.
Example
Here is a small example with a custom vertical list.
import SwiftUI
struct PlaylistItem: Identifiable, Sendable {
let id: UUID
var title: String
}
struct PlaylistView: View {
@State private var items: [PlaylistItem] = [
PlaylistItem(id: UUID(), title: "Intro"),
PlaylistItem(id: UUID(), title: "Main theme"),
PlaylistItem(id: UUID(), title: "Outro")
]
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(items) { item in
Text(item.title)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.reorderable()
}
.padding()
.reorderContainer(for: PlaylistItem.self) { difference in
items.apply(difference: difference)
}
}
}
}The important detail is the placement.
reorderable() is attached to the ForEach. reorderContainer is attached to the LazyVStack.
SwiftUI handles the drag interaction. Your job is to apply the ReorderDifference to the data source.
The model still belongs to you
This API does not magically persist a new order somewhere in your app. The move closure gives you a ReorderDifference. You decide what to do with it. In the simplest case, you apply it directly to an array:
.reorderContainer(for: PlaylistItem.self) { difference in
items.apply(difference: difference)
}That closure might update a view model, a SwiftData model, a local ordering field, or a server-backed order. The API does not force everything into IndexSet plus destination offset.
That matters because reorderable UI is not always a plain array problem. Sometimes the visible order is derived from persisted sort indexes. Sometimes items belong to sections. Sometimes a move is allowed only if the destination accepts that item.
The new API gives SwiftUI enough structure to describe the move, but leaves the actual model mutation where it should be: in your app.
Beyond List
The old List.onMove API was fine for classic edit-mode lists. But many modern SwiftUI interfaces are not built as plain lists.
Think about dashboards, photo grids, favorite actions, horizontally scrolling cards, board-like interfaces, custom layouts, or compact widgets inside a larger screen. These UIs still need native-feeling reordering, but List is often the wrong container.
Apple shows this API working with containers such as lists, stacks, grids, and custom layouts. The code shape stays almost the same because the reorder behavior is not tied to one specific container type.
Enabling and disabling reordering
reorderContainer also has an isEnabled parameter.
That gives you a clean way to keep reorder behavior conditional. For example, you might allow rearranging only in edit mode:
.reorderContainer(
for: PlaylistItem.self,
isEnabled: isEditing
) { difference in
items.apply(difference: difference)
}This is nicer than making the row gesture conditional by hand. The container still owns the interaction scope, and your state decides whether that interaction is currently active.
Manually applying ReorderDifference
For simple arrays, the shortest version is still this:
.reorderContainer(for: Card.self) { difference in
cards.apply(difference: difference)
}But the move closure does not force you to use apply(difference:). You can inspect the ReorderDifference and update the collection yourself.
Here is the same idea written manually:
import SwiftUI
struct Card: Identifiable, Sendable {
let id: UUID
var title: String
}
struct CardDeckView: View {
@State private var cards: [Card] = [
Card(id: UUID(), title: "Ace"),
Card(id: UUID(), title: "King"),
Card(id: UUID(), title: "Queen"),
Card(id: UUID(), title: "Jack")
]
var body: some View {
HStack(spacing: 12) {
ForEach(cards) { card in
Text(card.title)
.frame(width: 80, height: 110)
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.reorderable()
}
.padding()
.reorderContainer(for: Card.self) { difference in
guard let source = difference.sources.first else {
return
}
guard let sourceIndex = cards.index(cardID: source) else {
return
}
let destination = difference.destination
let removedCard = cards.remove(at: sourceIndex)
switch destination.position {
case .before(let beforeID):
guard let destinationIndex = cards.index(cardID: beforeID) else {
cards.insert(removedCard, at: min(sourceIndex, cards.endIndex))
return
}
cards.insert(removedCard, at: destinationIndex)
case .end:
cards.insert(removedCard, at: cards.endIndex)
}
}
}
}
private extension Array where Element == Card {
func index(cardID: Card.ID) -> Int? {
firstIndex { $0.id == cardID }
}
}
This example follows the same basic flow:
guard let source = difference.sources.first else {
return
}First, we take the moved item ID from difference.sources.
guard let sourceIndex = cards.index(itemID: source) else {
return
}Then we find that item in the current collection.
let destination = difference.destination
let removedCard = cards.remove(at: sourceIndex)After that, we remove the item from its old position and inspect the destination. The destination can point before another item:
case .before(let beforeID):
guard let destinationIndex = cards.index(cardID: beforeID) else {
cards.insert(removedCard, at: min(sourceIndex, cards.endIndex))
return
}
cards.insert(removedCard, at: destinationIndex)Or it can point to the end of the collection:
case .end:
cards.insert(removedCard, at: cards.endIndex)This is more verbose than apply(difference:), of course. It is useful when the move has app-specific rules. Maybe some cards cannot be moved. Maybe the order is stored as a separate sortIndex. Maybe the collection is backed by a view model or persistence layer. In those cases, ReorderDifference gives you enough information to translate the UI action into your own model update.
Multiple collections
The single-collection overload is the one you will probably reach for first. But SwiftUI also has a related shape for multiple collections. Apple uses this in the drag-and-drop code-along with a Solitaire example, where cards can move between piles.
In that kind of UI, the reorder operation needs to know more than “which item moved.” It also needs to know which collection the item came from and where it is going.
That is where the collection identifier version matters:
.reorderContainer(for: CardValue.self, in: Card.Group.self) { difference in
game.moveCards(difference: difference)
}And the reorderable content can provide a matching collection ID:
ForEach(cards, id: \.value) { card in
CardView(card: card)
}
.reorderable(collectionID: Card.Group.pile(index))What this does not replace
This is still not a replacement for every drag-and-drop scenario. If your app needs to accept external data, customize copy versus move behavior, validate drops, or insert items from another source, you are still in the broader drag-and-drop API family. Apple shows dragContainer, dragConfiguration, dropDestination, and dropConfiguration next to the reordering API in the same code-along. You can read more about it here.
So I would not read reorderable() as “new drag and drop in one modifier.”
It is more specific than that. It solves the native reordering case. When the rules get more complex, you can combine it with the rest of SwiftUI’s drag-and-drop tools.
Final thoughts
The previous model made reordering feel like a feature of List. The new model makes it feel like a behavior of dynamic content inside a container. That is much closer to how SwiftUI apps are actually built.
