Developers
SwiftUI Reorder List in iOS 27: reorderable() and Swipe Actions
Drag to reorder any SwiftUI list, stack or grid in iOS 27 with reorderable() and reorderContainer, plus swipe actions outside List, on iPhone and iPhone Duo.
By AppleDuo EditorialPublished 7 min read
On this page
For years, the answer to "how do I let people reorder a SwiftUI list?" was the same: attach .onMove to a ForEach inside a List, add an EditButton, and translate index offsets back into your model. It only worked in List, and it came with edit mode. This guide covers the SwiftUI reorder list API in iOS 27, which works in any container and needs neither.
iOS 27 also lifts a second List-only limit: swipe actions. Both changes come from WWDC26 and are documented in Apple's SwiftUI reference. The post is based on that documentation, Apple's Code-along: Build powerful drag and drop in SwiftUI session and posts by iOS developer @sarunw. These APIs are new in iOS 27.0, and details can change between releases.
How do I reorder a SwiftUI list in iOS 27?
You need two modifiers. As @sarunw sums it up, iOS 27 gives ForEach its own drag to reorder: mark it .reorderable() and put .reorderContainer on the List. "No .onMove. No edit mode."
reorderable()goes on theForEach(anyDynamicViewContent) whose views people can move.reorderContainer(for:isEnabled:move:)goes on the container and defines the scope of the drag. You pass your item type, and amoveclosure that receives the change.
struct TaskListView: View {
@State private var tasks: [TaskItem] = TaskItem.samples
var body: some View {
List {
ForEach(tasks) { task in
TaskRow(task: task)
}
.reorderable()
}
.reorderContainer(for: TaskItem.self) { difference in
tasks.apply(difference)
}
}
}
TaskItem is Identifiable, and Apple's guide asks for an ID type that is Hashable and Sendable. The apply(_:) call is our own helper, shown in the next section; SwiftUI doesn't document one for arrays.
Apple describes the interaction itself: a person lifts a view with a drag gesture, the system puts a placeholder where it can drop, the placeholder follows the drag, and on drop the system calls move. You don't write gesture code or animations.
The container doesn't have to be a List. Apple's documentation says it can be a list, stack, grid or custom layout. Apple's own example applies .reorderContainer to a plain VStack, and the WWDC26 session uses an HStack of playing cards.
How does the reorder closure update my data?
The move closure receives a ReorderDifference. It describes the change by identity, not by index:
| Property | Type | Meaning |
|---|---|---|
sources | [ItemID] | IDs of the moved items, in the order the person selected them |
destination.position | .before(_:) or .end | Insert before the item with that ID, or append to the end |
destination.collectionID | Your collection ID type | Which section receives the items (multi-collection containers) |
Working with IDs has a practical benefit. Apple's guide notes that you can move the items "even if other changes to the array, such as a cloud sync, occur simultaneously," because an ID still points to the right element after the array changes. An IndexSet from .onMove doesn't.
Here's one way to apply a single-collection difference to an array. It removes the moved items and reinserts them, in selection order, at the destination:
@available(iOS 27.0, *)
extension Array where Element: Identifiable, Element.ID: Sendable {
mutating func apply(
_ difference: ReorderDifference<Element.ID, ReorderableSingleCollectionIdentifier>
) {
let moved = difference.sources.compactMap { id in
first { $0.id == id }
}
removeAll { difference.sources.contains($0.id) }
switch difference.destination.position {
case .before(let id):
let index = firstIndex { $0.id == id } ?? endIndex
insert(contentsOf: moved, at: index)
case .end:
append(contentsOf: moved)
}
}
}
The generic type matches the single-collection overload, whose closure takes ReorderDifference<Item.ID, ReorderableSingleCollectionIdentifier>. Treat the helper as a starting point and test it against your model.
Reordering across sections
For sectioned data, give each ForEach an identifier with reorderable(collectionID:), and use reorderContainer(for:in:isEnabled:move:) with the type that identifies sections. People can then move items within and between sections, and destination.collectionID tells you which one received them.
List {
ForEach(model.sections) { section in
Section(section.name) {
ForEach(section.reminders) { reminder in
ReminderView(reminder)
}
.reorderable(collectionID: section.id)
}
}
}
.reorderContainer(for: Reminder.self, in: ReminderModel.Section.ID.self) { difference in
model.apply(difference: difference)
}
This mirrors Apple's documentation sample; model.apply(difference:) is your model's code. If your ForEach uses an ID other than id, the itemID: overloads take a key path, such as \.databaseID.
How is reorderable() different from onMove and EditButton?
The older pattern still works. It's worth knowing exactly what changes before you migrate:
.onMove + EditButton | .reorderable() + .reorderContainer | |
|---|---|---|
| Introduced | iOS 13 | iOS 27.0 |
| Containers | List | List, stack, grid or custom layout |
| How it starts | Drag handles, typically shown in edit mode | System drag gesture |
| Closure input | (IndexSet, Int) offsets | ReorderDifference with item IDs |
| Moving between sections | Each ForEach has its own onMove | reorderable(collectionID:), one closure |
| Multiple items | Not described in the docs | Sources listed in selection order |
| Turn it off | Pass nil to onMove | isEnabled: false |
| Drag in from other apps | Separate onInsert or dropDestination | Pair with dropDestination and reorderDestination |
Hacking with Swift's long-running guide shows the classic setup: onMove(perform:) on the ForEach, plus an EditButton in the toolbar to make the drag handles appear. Apple's onMove documentation describes its closure arguments as offsets in the underlying collection, which is why the old code usually ends with move(fromOffsets:toOffset:).
The isEnabled parameter is handy for real apps. Apple's guide suggests turning reordering off while you sync, for example isEnabled: !model.isSaving.
The session adds one design tip from its Solitaire example: to keep some items fixed, split them into a second ForEach without .reorderable(). Only the content you mark can be lifted. Apple's docs don't describe combining .onMove and .reorderable() on the same ForEach, so pick one.
Can I use swipe actions outside a List in iOS 27?
Yes. @sarunw puts it simply: "Swipe actions used to need a List row. In iOS 27 any view can have them." The piece that makes it work is swipeActionsContainer().
You still write .swipeActions on each row, exactly as in a List. Then you apply .swipeActionsContainer() to the ScrollView, LazyVStack or other container that holds the rows:
ScrollView {
LazyVStack {
ForEach(items) { item in
ItemRow(item)
.swipeActions {
Button("Delete", role: .destructive) {
delete(item)
}
}
}
}
}
.swipeActionsContainer()
Apple's documentation lists what the container coordinates:
- Only one row's swipe actions are revealed at a time.
- Scrolling the container dismisses any open actions.
- Tapping outside the active row dismisses its actions.
List already does all of this, so Apple calls applying the modifier to a List a no-op. Majid Jabrayilov's write-up reaches the same rule: anything other than List needs the container modifier.
iOS 27 also adds swipeActions(edge:allowsFullSwipe:content:onPresentationChanged:). Its closure receives true when a row's actions appear and false when they're dismissed. Apple suggests using it to dim the row or update surrounding UI while actions are showing.
Known issues to check
Two developer reports on Apple's forums are worth testing against your own build. One says that in right-to-left layouts the actions appear on the mirrored edge but the gesture still expects the left-to-right direction. The other says that outside List, the swipe actions' content isn't re-evaluated when state changes, so a Pin title may not flip to Unpin until the row is recreated. Neither thread had an Apple reply at the time of writing.
Do these APIs back-deploy to earlier iOS versions?
No. reorderable(), every reorderContainer overload, swipeActionsContainer() and the onPresentationChanged overload are documented as available from iOS 27.0, iPadOS 27.0, Mac Catalyst 27.0, macOS 27.0, visionOS 27.0 and watchOS 27.0. The WWDC26 session describes the reordering APIs as "newly available on all Apple platforms that support drag and drop."
If you support iOS 26 or earlier, branch with #available and keep the old path:
@ViewBuilder
private var taskList: some View {
if #available(iOS 27.0, *) {
List {
ForEach(tasks) { TaskRow(task: $0) }
.reorderable()
}
.reorderContainer(for: TaskItem.self) { tasks.apply($0) }
} else {
List {
ForEach(tasks) { TaskRow(task: $0) }
.onMove { tasks.move(fromOffsets: $0, toOffset: $1) }
}
.toolbar { EditButton() }
}
}
On older versions, swipe actions outside List have no fallback; Nil Coalescing notes they had no effect on rows in a LazyVStack or grid before iOS 27. Keep those rows in a List there, or offer the same actions in a context menu.
How does this help on iPhone Duo?
On iPhone Duo, the inner display reports a regular horizontal size class, like an iPad, while the outer display stays compact (see our guide to iPhone Duo size classes). Many apps answer that extra width with a grid or a custom layout instead of a single-column List.
Until iOS 27, that choice cost you features. A LazyVGrid of cards had no built-in reordering and no swipe actions, so moving to a grid on the inner display meant rebuilding both by hand. Now the same .reorderable() and .swipeActions code works whether a size class switch shows a List on the outer display or a grid on the inner one:
let columns = [GridItem(.adaptive(minimum: 220))]
ScrollView {
LazyVGrid(columns: columns) {
ForEach(boards) { board in
BoardCard(board: board)
}
.reorderable()
}
}
.reorderContainer(for: Board.self) { boards.apply($0) }
Apple's guide notes that reordering only covers moves inside the container. Dragging items out to other parts of your app, or in from other apps, is a separate step with dragContainer(for:in:_:) and dropDestination(for:isEnabled:action:). If the inner display shows two panes side by side with ArrangementView, our ArrangementView guide covers the split itself.
Key takeaways
- In iOS 27,
.reorderable()on aForEachplus.reorderContainer(for:)on its container gives you drag to reorder with no edit mode and no.onMove. - The container can be a
List, stack, grid or custom layout. - The
moveclosure receives aReorderDifference: moved IDs insources, and adestinationof.before(_:)or.end. Apply it to your array yourself. - Use
reorderable(collectionID:)andreorderContainer(for:in:)to move items between sections. - Swipe actions now work outside
List; add.swipeActionsContainer()to theScrollViewor layout. - Everything here is iOS 27.0 and later, so gate it with
#availableand keep.onMoveas the fallback.
Lists are only half of a polished UI; the text inside the rows matters too. Next, read our guide to SwiftUI line height in iOS 26, and see Built for the fold for apps and games that already make the most of iPhone Duo.
Frequently asked questions
How do I reorder a SwiftUI List in iOS 27 without edit mode?
Apply .reorderable() to the ForEach inside the List and .reorderContainer(for:) to the List itself, passing your item type. People can then drag rows directly, and the move closure gives you a ReorderDifference to apply to your data. No EditButton or onMove is involved.
Does reorderable() work in LazyVStack and LazyVGrid?
Yes. Apple's documentation says a reorderable container can be a list, stack, grid or custom layout. Put .reorderContainer on that container and .reorderable() on the ForEach inside it.
What is ReorderDifference in SwiftUI?
It's the value the move closure receives at the end of a drag. Its sources property lists the IDs of the moved items in the order the person selected them, and its destination says where they go: before a given item or at the end of a collection.
Can I use swipe actions outside a List in iOS 27?
Yes. Add .swipeActions to each row as usual and apply .swipeActionsContainer() to the enclosing ScrollView or layout. The container makes sure only one row is open at a time and that scrolling or tapping elsewhere closes it.
Do reorderable() and swipeActionsContainer() back-deploy to iOS 26?
No. Both are documented as available from iOS 27.0, so apps that support earlier versions need an if #available check with a fallback such as onMove in a List.
Sources
- reorderable() · Apple Developer Documentation
- reorderContainer(for:isEnabled:move:) · Apple Developer Documentation
- reorderContainer(for:in:isEnabled:move:) · Apple Developer Documentation
- Reordering items in lists, stacks, grids, and custom layouts · Apple Developer Documentation
- ReorderDifference · Apple Developer Documentation
- onMove(perform:) · Apple Developer Documentation
- swipeActionsContainer() · Apple Developer Documentation
- swipeActions(edge:allowsFullSwipe:content:onPresentationChanged:) · Apple Developer Documentation
- Code-along: Build powerful drag and drop in SwiftUI (WWDC26) · Apple Developer
- swipeActionsContainer() does not mirror the swipe gesture in right-to-left layouts · Apple Developer Forums
- iOS 27: swipeActions outside List never re-evaluates its content closure · Apple Developer Forums
- iOS 27 gives ForEach its own drag to reorder: .reorderable() and .reorderContainer · @sarunw on X
- No .onMove. No edit mode. · @sarunw on X
- Swipe actions used to need a List row; in iOS 27 any view can have them · @sarunw on X
- New SwiftUI APIs for reordering and drag and drop on iOS 27 · Nil Coalescing
- Custom scroll layouts with swipe actions in SwiftUI on iOS 27 · Nil Coalescing
- Swipe actions outside of List in SwiftUI · Swift with Majid
- How to let users move rows in a list · Hacking with Swift
Make the fold part of your app.
We showcase apps and games that do something real with iPhone Duo. Send us yours with a link to it on the App Store, or see what other developers have built.