Issue #1064
SwiftUI now ships a built-in way to let people drag items around in a list, stack, or grid, without writing gesture code or tracking drag offsets by hand.
The two pieces you need
Reordering splits into two responsibilities. The ForEach that generates your views declares which views are reorderable. The container holding them declares where reordering is allowed and what to do when it happens. Keeping these separate means the same ForEach works inside a VStack, a LazyVGrid, or a custom Layout.
Your data needs an identifier that is both Hashable and Sendable, since the system tracks items while a drag is in progress. Conforming to Identifiable with a Sendable id usually covers this.
struct Task: Identifiable, Hashable, Sendable {
let id: UUID
var title: String
var isDone: Bool
}
Reordering a single list
Say you have a simple task list backed by an array. Add .reorderable() to the ForEach that builds the rows, and .reorderContainer(for:move:) to the stack that wraps it.
struct TaskListView: View {
@State private var tasks: [Task] = Task.sampleData
var body: some View {
VStack {
ForEach(tasks) { task in
TaskRow(task: task)
}
.reorderable()
}
.reorderContainer(for: Task.self) { difference in
tasks.move(with: difference)
}
}
}
The move closure receives a ReorderDifference, describing exactly which items moved and where they landed, even if the array changed for other reasons mid-drag, such as a background sync. Apply the difference to your array and SwiftUI animates the rows into place. There is no need to compute source and destination indices yourself, unlike with older approaches such as onMove.
Reordering across sections
Most real apps have groups, not one flat list: a photo app has albums, a notes app has folders, a kanban board has columns. SwiftUI supports this by letting each section carry its own collection identifier.
Use .reorderable(collectionID:) on each section’s ForEach instead of .reorderable(), tagging it with the id of the section it belongs to.
struct AlbumBoardView: View {
@State private var model: AlbumModel
var body: some View {
ScrollView {
LazyVStack {
ForEach(model.albums) { album in
Section(album.title) {
ForEach(album.photos) { photo in
PhotoThumbnail(photo: photo)
}
.reorderable(collectionID: album.id)
}
}
}
}
.reorderContainer(for: Photo.self, in: Album.ID.self) { difference in
model.apply(difference)
}
}
}
Now a drag can move a photo within an album, or from one album to another, and the same move closure handles both. ReorderDifference tells you which collection the item came from and which it ended up in, so your model update stays in one place.
Letting items leave the container
Reordering inside a container is only half of what drag and drop usually needs. People also expect to drag a row into another window or a different app. That takes two more modifiers on top of what you already have.
.dragContainer(for:in:_:) tells SwiftUI what to hand over when an item is dragged away, mapping an item’s id to a transferable value. .dropDestination(for:isEnabled:action:), placed above the reorder container, catches drops coming from outside.
VStack {
ForEach(tasks) { task in
TaskRow(task: task)
}
.reorderable()
}
.reorderContainer(for: Task.self) { difference in
tasks.move(with: difference)
}
.dragContainer(for: Task.self) { id in
tasks.first { $0.id == id }.map { [$0] } ?? []
}
.dropDestination(for: Task.self) { items, session in
if let destination = session.reorderDestination(for: Task.ID.self),
let index = tasks.firstIndex(where: destination.matches) {
tasks.insert(contentsOf: items, at: index)
} else {
tasks.append(contentsOf: items)
}
return true
}
Inside the drop action, reorderDestination(for:) tells you where the drop landed relative to existing rows, such as before a given item or at the end. That lets one drop handler support both inserting mid-list and appending at the bottom, without extra hit-testing.
You can also accept drops on individual rows rather than the whole container, useful for dropping a file onto a folder row. Attach .dropDestination directly to the row inside the ForEach, and use isEnabled to restrict which rows accept a drop.
ForEach(items) { item in
ItemRow(item)
.dropDestination(for: Item.self, isEnabled: item.isFolder) { dropped, _ in
model.move(dropped, into: item)
}
}
.reorderable()
Turning reordering off when it does not make sense
Sometimes you need to pause reordering, for example while a network sync writes to the same array. Rather than removing the modifiers, pass a Boolean to the isEnabled parameter both reorderContainer variants accept.
.reorderContainer(for: Photo.self, isEnabled: !model.isSyncing) { difference in
model.apply(difference)
}
Flip isSyncing back to false once the sync finishes, and reordering is available again with no other change needed, a smaller footprint than the old pattern of toggling EditMode or hiding drag handles across the view hierarchy.
These modifiers are new in SwiftUI, so check availability with if #available if you support older OS versions, and keep a fallback such as onMove for those cases.
Start the conversation