Another year, another WWDC has come and gone. And as always, this year too, WWDC26 brought lots of improvements and novelties in frameworks, SDKs, and tools. SwiftUI, a field of great advancements, could not stay out of this, so the announcement of new APIs made the framework a more powerful, complete, and better tool we all love using. Among all the great new stuff about SwiftUI, there are fresh APIs that handle reordering of items in collections that we display in lists and grids.
Reordering is becoming a straightforward task, with custom workarounds implemented specifically for this purpose belonging soon to the past. And it’s not just the reordering that is made easy, but the drag and drop received an overall boost, however that’s a topic to discuss in a different tutorial. For now, let’s jump into some new cool reordering APIs that will make our lives as developers a lot easier.
Note: At the time of writing this post, Xcode 27 is still in beta.
Enabling list items for reordering
To keep the demonstration as simple as possible and to the point, suppose we have a list of tasks, where each task is represented by the following type:
|
1 2 3 4 5 6 |
struct TaskItem: Identifiable { let id = UUID() var title: String } |
Besides the id property that mandatorily exists in TaskItem since it conforms to Identifiable, there’s only the title that will contain each task’s description. With that type in place, let’s add a handful of tasks in an array:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct ContentView: View { @State private var tasks = [ TaskItem(title: "Write SwiftUI tutorial"), TaskItem(title: "Review export code"), TaskItem(title: "Reply to newsletter emails"), TaskItem(title: "Update project roadmap"), TaskItem(title: "Prepare tutorial screenshots"), TaskItem(title: "Publish blog post") ] … } |
Listing task items is straightforward with a List and a ForEach container:
|
1 2 3 4 5 6 7 8 9 10 |
List { ForEach(tasks) { task in Text(task.title) .padding(.vertical, 8) } } .listStyle(.insetGrouped) .padding() |
Even though tasks are listed, they cannot be reordered with just the above code. Nevertheless, SwiftUI now introduces the reorderable modifier, which simply enables list items to be reordered easily when dragged. The system is responsible for all the move animations and visual updates, hiding all that complexity under the framework’s hood.
The reorderable modifier is applied to the container view that lists the items, in that case the ForEach:
|
1 2 3 4 5 6 |
ForEach(tasks) { task in … } .reorderable() |
However, reorderable alone is not enough even to enable the move animation. That’s the first half of the job, because we also have another, more important task before reordering works; to describe the logic, and how the items in the data source collection should be updated upon reordering.
Applying reordering logic in the reorderContainer
There’s another view modifier that has to be applied to the parent view of the ForEach, in this example to the List. That modifier, also new in SwiftUI, is named reorderContainer, and besides making the target view a container for reordering, it also provides the space to describe in code how reordering affects the original collection, and how to handle reordered items:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
List { ForEach(tasks) { task in … } .reorderable() } // … other modifiers… .reorderContainer(for: TaskItem.self) { difference in } |
There are various overloads of the modifier, but the one that will probably be used most often is the one you see in the above code block. It accepts two arguments, the type of the reordered items, and a closure with a parameter value describing the changes made while reordering.
Some examples use ready-made collection helpers to apply reordering changes. In this tutorial, we’ll implement the update logic ourselves so we can better understand what the difference argument provides.
difference has two useful properties accessible:
sources: A collection that contains the identifier values of the items we reorder. In case of multiple reordering, identifiers exist in the order they were selected.destination: Another property that describes the target position and collection of the items being reordered. In this post, we’ll deal with the former only.
Let’s see how to use these in our particular example here.
Since we keep things simple and we allow reordering one item only at a time, the first task in the reorderContainer is to get the index of that item in the original tasks collection:
|
1 2 3 4 5 6 7 8 9 10 |
.reorderContainer(for: TaskItem.self) { difference in guard let sourceItemID = difference.sources.first, let sourceIndex = tasks.firstIndex( where: { $0.id == sourceItemID } ) else { return } } |
First, we extract the item’s identifier from the difference.sources array to the sourceItemID. Then, we use sourceItemID to find the index (position) of the reordered item in tasks, which is stored to the sourceIndex constant.
The next move is to remove the source item from the tasks collection, so we can add it back to the new position. But before that, it’s always a good idea to work with a copy of the original array, so in case updating it fails for some reason, not to lose original data:
|
1 2 3 4 5 6 7 8 9 |
.reorderContainer(for: TaskItem.self) { difference in … var updatedTasks = tasks let sourceItem = updatedTasks.remove(at: sourceIndex) } |
From now on we’ll act on updatedTasks, and only when modifications are successful we’ll put everything back to tasks. It’s now safe to remove the moved item from the collection, and keep it to the sourceItem.
Let’s continue by focusing on the destination of the reordered item, and update the collection accordingly. When it comes to that, there are two possible positions that a moved item can end up in:
- Before another item.
- To the end of the collection.
We handle these two cases like so:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
.reorderContainer(for: TaskItem.self) { difference in … switch difference.destination.position { case .before(let itemID): case .end: } } |
See the two cases before and end of the position property, available through the difference.destination object. The former, before case, provides the identifier of the target item, before of which the reordered item should go. Using it, we can find its position in the array, and then insert the moved item into that position:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
switch difference.destination.position { case .before(let itemID): guard let destinationIndex = updatedTasks.firstIndex( where: { $0.id == itemID} ) else { return } updatedTasks.insert(sourceItem, at: destinationIndex) … } |
On the other hand, if the moved item lands at the end of the list, then we simply append it to the collection:
|
1 2 3 4 5 6 7 8 |
switch difference.destination.position { … case .end: updatedTasks.append(sourceItem) } |
There’s one thing left to do; to assign updatedTasks back to tasks and reflect the changes made after reordering:
|
1 2 3 4 5 6 7 |
.reorderContainer(for: TaskItem.self) { difference in … tasks = updatedTasks } |
The full code is this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
List { ForEach(tasks) { task in Text(task.title) .padding(.vertical, 8) } .reorderable() } .listStyle(.insetGrouped) .padding() .reorderContainer(for: TaskItem.self) { difference in guard let sourceItemID = difference.sources.first, let sourceIndex = tasks.firstIndex( where: { $0.id == sourceItemID } ) else { return } var updatedTasks = tasks let sourceItem = updatedTasks.remove(at: sourceIndex) switch difference.destination.position { case .before(let itemID): guard let destinationIndex = updatedTasks.firstIndex( where: { $0.id == itemID } ) else { return } updatedTasks.insert(sourceItem, at: destinationIndex) case .end: updatedTasks.append(sourceItem) } tasks = updatedTasks } |
Note: The code in reorderContainer is customized to the needs of this example and the sake of demonstrating the new APIs. The actual code added to the closure of this modifier always depends on the needs of the app and the details of the project under implementation.
Thanks to both the reorderable and the reorderContainer modifiers, we can now reorder without any additional hassle:
Reordering grid items
Previous parts demonstrate how to reorder items inside a List, but it’s literally exactly the same to reorder items in grids. In fact, simply replacing the List with the grid view we want is pretty much all we need, and I’ll show you that.
In the code presented in the end of the last part, we’ll replace List with a LazyVGrid. Just that is enough to make reordering work in the grid, given that we keep reorderable modifier. It’s not even necessary to touch the code in the reorderContainer.
Note: For better visual representation, Text view’s modifiers have been updated, while the listStyle modifier has been removed too, as it’s no longer relevant.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
private let columns = [GridItem(.adaptive(minimum: 150), spacing: 12)] LazyVGrid(columns: columns, spacing: 20) { ForEach(tasks) { task in Text(task.title) .padding() .frame(height: 120) .frame(maxWidth: .infinity) .background(.quaternary, in: RoundedRectangle(cornerRadius: 12)) } .reorderable() } .padding() .reorderContainer(for: TaskItem.self) { difference in // … same as above … } |
Wrapping up
Out of the box reordering has been a missing feature in SwiftUI, and it’s finally here. Enabling reordering in both lists and grids is the result of applying two modifiers and implementing the backing logic, but advancements do not stop here. Regardless, enjoy reordering items in lists and grids. Thanks for reading!