How to Reorder List rows in SwiftUI List

⋅ 4 min read ⋅ SwiftUI List

Table of Contents

SwiftUI List provides an easy way to enable Reorder functionality by applying the onMove(perform:) modifier to items in a List.

onMove(perform:) accept a closure that gets called when items in the List view are moved.

A closure takes two arguments, an IndexSet of moving items and a new destination position in Int.

.onMove { fromOffsets: IndexSet, toOffset: Int in

}

How to Reorder List rows in SwiftUI List

To enable the SwiftUI List rows reordering, you need to do the following steps.

  1. Create a data source.
  2. Populate a List view using ForEach.
  3. Apply onMove(perform:) modifier to the ForEach.
  4. Manually move items in the onMove's closure.

Create Data Source

Since you want to move an item in a List view, your data source must be mutable.

In this example, I use a @State variable as a data source.

@State private var contacts = [
"John",
"Alice",
"Bob",
"Foo",
"Bar"
]

Create a List view with ForEach

The onMove modifier only works with ForEach, so we need to populate our List view using ForEach.

struct ContentView: View {
@State private var contacts = [
"John",
"Alice",
"Bob",
"Foo",
"Bar"
]

var body: some View {
List {
ForEach(contacts, id: \.self) { contact in
Text(contact)
}
}
}
}

Apply onMove modifier on ForEach

Apply the onMove modifier on ForEach to enable the reorder function.

struct ContentView: View {
@State private var contacts = [
"John",
"Alice",
"Bob",
"Foo",
"Bar"
]

var body: some View {
List {
ForEach(contacts, id: \.self) { contact in
Text(contact)
}.onMove { from, to in
// TODO: move the data source.
}
}
}
}

Move items in Data Source

The onMove modifier doesn't automatically move an item in your data source. You have to do it yourselves.

The onMove's closure takes two arguments, an IndexSet of moving items and a new destination position in Int.

Swift Array has a move(fromOffsets:toOffset:) method that match the two arguments we got.

extension MutableCollection {
mutating func move(
fromOffsets source: IndexSet,
toOffset destination: Int
)
}

We mutate the data source inside the onMove's closure, contacts.move(fromOffsets: from, toOffset: to).

struct ContentView: View {
@State private var contacts = [
"John",
"Alice",
"Bob",
"Foo",
"Bar"
]

var body: some View {
List {
ForEach(contacts, id: \.self) { contact in
Text(contact)
}.onMove { from, to in
contacts.move(fromOffsets: from, toOffset: to)
}
}
}
}

That's all we need to do to enable List row reordering.

What does onMove do

By enabling the reordering function by attaching the onMove(perform:) modifier, users can reorder the list item by Long-press on the row that you want to move, then drag it to the destination.

Long-press

Users can Long-press on any row to move that particular row.

Long-press to reorder.
Long-press to reorder.

Items in EditMode

In the Edit mode, SwiftUI also shows a drag handler on the trailing side of the row content to indicate that list rows can be moved around.

There are two options to enter the Edit mode.

  1. Using the EditButton
  2. Setting the .editMode environment value

I will use EditButton in this case.

struct ContentView: View {
@State private var contacts = [
"John",
"Alice",
"Bob",
"Foo",
"Bar"
]

var body: some View {
NavigationView {
List {
ForEach(contacts, id: \.self) { contact in
Text(contact)
}.onMove { from, to in
contacts.move(fromOffsets: from, toOffset: to)
}
}
.toolbar {
// 1
EditButton()
}
}

}
}

I added the edit button on a navigation bar 1 to allow users to enter the Edit mode.

Once you enter the edit mode by tapping the "Edit" button, you will see the drag handler at the end of each row.

Reorder items in Edit Mode.
Reorder items in Edit Mode.

How to conditionally Disable the Reorder ability

If you want to disable the reorder ability, you can do that by passing nil to the onMove.

ForEach(contacts, id: \.self) { contact in
Text(contact)
}
.onMove(perform: nil)

Here is an example where we control reorder ability with the boolean value, enableMove.

struct ReorderExample: View {
@State private var contacts = [
"John",
"Alice",
"Bob",
"Foo",
"Bar"
]

// 1
@State private var enableMove = true

var body: some View {
NavigationView {
List {
Toggle("Enable Move", isOn: $enableMove)
ForEach(contacts, id: \.self) { contact in
Text(contact)
}
// 2
.onMove(perform: enableMove ? move: nil)
}
.toolbar {
EditButton()
}
}

}

// 3
func move(from: IndexSet, to: Int) {
contacts.move(fromOffsets: from, toOffset: to)
}
}

1 A state variable use to control reorder ability.
2 We use enableMove to conditionally disable reorder ability. We disable it by set perform closure as nil.
3 We extract a move logic out as a separate function to improve readability.

Disable the reorder ability by passing nil to the onMove.
Disable the reorder ability by passing nil to the onMove.

Read more article about SwiftUI, List, or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
Swift mod operator (%)

There might be several reasons that cause this error. I will share the solution that works for me.

Next
What is the difference between #available and @available

#available and @available are features related to the availability of APIs. They are tools that mean to use together. Let's learn the difference and when to use them.

← Home