If you are a modern iOS Developer, you have surely left behind the days of dealing with UITableViewDataSource and endless delegate methods to achieve simple tasks. The evolution of Swift programming has brought us declarative tools that make development much faster and less prone to errors.
One of the most common interactions in any application is the ability to modify data collections. In this article, we are going to explore in depth how to implement editable lists in SwiftUI, allowing users to delete and reorder items with native gestures. Best of all, thanks to SwiftUI, we will learn how to apply these concepts cross-platform, using Xcode and Swift so your code works perfectly on iOS, macOS, and watchOS.
1. The Power of Interaction: What Makes a List Editable?
In SwiftUI, a List is much more than a simple view container. It is designed to integrate tightly with your application’s state. To create editable lists in SwiftUI, we rely on specific modifiers that are applied to the iterated items (usually inside a ForEach) and not to the list itself.
The two fundamental pillars of list editing are:
- Deletion (Delete): Allowing the user to swipe a row to delete it or tap a delete button in edit mode.
- Reordering (Move): Allowing the user to drag rows to change their logical order within the collection.
For SwiftUI to manage these animations and state changes correctly, our data must be backed by a robust model.
2. Setting Up the Environment in Xcode
Before writing the logic, let’s prepare our canvas.
- Open Xcode and select “Create a new Xcode project”.
- Go to the Multiplatform tab and select the App template. This ensures our codebase will be compatible with the entire ecosystem.
- Name your project, for example,
EditableListMastery. - Make sure the interface is set to SwiftUI and the language is Swift.
By using the cross-platform approach, Xcode configures the necessary targets so we can test the same list view in an iPhone simulator, on our Mac, and on the Apple Watch.
3. Building the Data Model
As any good iOS Developer knows, the user interface is just a reflection of the underlying data. Let’s create a simple To-Do List application.
Create a new Swift file named TaskItem.swift:
import Foundation
// Our model must conform to Identifiable to work well in List and ForEach
struct TaskItem: Identifiable, Equatable {
let id = UUID()
var title: String
var isCompleted: Bool = false
}
The Identifiable protocol is crucial here. When we implement editable lists in SwiftUI, the framework needs to know exactly which item is being moved or deleted. The unique id takes care of this, preventing erratic behaviors or unexpected crashes that used to happen in UIKit when the data source lost synchronization with the view.
4. Implementing Row Deletion (Swipe to Delete)
Let’s create our main view and add the deletion functionality. For this, we use the .onDelete(perform:) modifier.
Open your ContentView.swift and implement the following:
import SwiftUI
struct ContentView: View {
// We use @State so SwiftUI reacts to changes in the array
@State private var tasks: [TaskItem] = [
TaskItem(title: "Study Swift programming"),
TaskItem(title: "Master Xcode 15"),
TaskItem(title: "Implement editable lists in SwiftUI"),
TaskItem(title: "Upload app to the App Store")
]
var body: some View {
NavigationStack {
List {
// It is vital to use ForEach inside the List to enable editing
ForEach(tasks) { task in
Text(task.title)
.font(.body)
}
// The onDelete modifier is applied to the ForEach, not the List
.onDelete(perform: deleteTasks)
}
.navigationTitle("My Tasks")
}
}
// Function to handle deletion
func deleteTasks(at offsets: IndexSet) {
tasks.remove(atOffsets: offsets)
}
}
Understanding IndexSet
In Swift programming, when you use .onDelete, SwiftUI doesn’t pass you the object itself, but an IndexSet. An IndexSet is a collection that represents the indices of the items the user wants to delete (it can be more than one if you are in multi-edit mode). The standard Swift library provides the remove(atOffsets:) method on Arrays, which makes this operation trivial and a one-liner.
If you test this in the iOS simulator, you will see that you can already swipe from right to left on any row to reveal the red delete button. Pure magic with just a few lines of code!
5. Implementing Row Reordering (Drag to Move)
Now, let’s give the user the ability to prioritize their tasks by moving them around. For this, we will add the .onMove(perform:) modifier.
Modify your ContentView.swift to add this functionality:
import SwiftUI
struct ContentView: View {
@State private var tasks: [TaskItem] = [
TaskItem(title: "Study Swift programming"),
TaskItem(title: "Master Xcode"),
TaskItem(title: "Implement editable lists in SwiftUI"),
TaskItem(title: "Upload app to the App Store")
]
var body: some View {
NavigationStack {
List {
ForEach(tasks) { task in
Text(task.title)
}
.onDelete(perform: deleteTasks)
.onMove(perform: moveTasks) // We add the move modifier
}
.navigationTitle("My Tasks")
.toolbar {
// We add the native EditButton to the navigation bar
EditButton()
}
}
}
func deleteTasks(at offsets: IndexSet) {
tasks.remove(atOffsets: offsets)
}
// Function to handle reordering
func moveTasks(from source: IndexSet, to destination: Int) {
tasks.move(fromOffsets: source, toOffset: destination)
}
}
The role of EditButton and moveTasks
Unlike .onDelete, which can be triggered by a swipe gesture, traditional visual reordering with the “hamburger” icons to the right of the row requires the list to be in Edit Mode.
To easily enable this mode in iOS, SwiftUI gives us the EditButton() view. By placing it in the .toolbar, SwiftUI automatically toggles the environment’s editing state (Environment editMode).
The moveTasks(from:to:) function receives the set of original indices (source) and the destination index (destination). Just like with deletion, collections in Swift have a native move(fromOffsets:toOffset:) method that safely handles the swap in your underlying array.
6. Adapting Editable Lists for macOS and watchOS
As true professionals, we don’t stop at the iPhone. The ecosystem is broad, and the behavior of editable lists in SwiftUI varies slightly depending on the operating system.
watchOS
On the Apple Watch, space is vital. The .onDelete modifier works natively and fantastic: the user simply swipes the row to delete.
However, the EditButton and complex reordering are not common or easy-to-use interaction patterns on such a small screen. Generally, for watchOS, we stick to swipe to delete to maintain a clean and usable interface.
You can use conditional compilation directives to hide the edit button on the watch:
.toolbar {
#if os(iOS)
EditButton()
#endif
}
macOS
On the Mac, interaction shifts toward the mouse and keyboard. In macOS, there is no native EditButton that triggers a visual edit mode with “hamburger” icons.
Instead, Mac users expect to be able to drag and drop items directly without entering an “edit mode,” or use the keyboard (backspace key) to delete selected rows.
To support deletion with the Backspace key on macOS, we need to manage list selection:
struct ContentView: View {
@State private var tasks: [TaskItem] = [ /* ... */ ]
@State private var selection: Set<UUID> = [] // State for multiple selection
var body: some View {
NavigationStack {
// We pass the selection binding to the List
List(selection: $selection) {
ForEach(tasks) { task in
Text(task.title)
.tag(task.id) // Essential for selection to work
}
.onDelete(perform: deleteTasks)
.onMove(perform: moveTasks)
}
.navigationTitle("My Tasks")
.toolbar {
#if os(iOS)
EditButton()
#elseif os(macOS)
// Manual button to delete selected items on macOS
Button(role: .destructive, action: deleteSelected) {
Label("Delete", systemImage: "trash")
}
.disabled(selection.isEmpty)
#endif
}
}
}
// ... deleteTasks and moveTasks functions ...
// Function specific to macOS
func deleteSelected() {
tasks.removeAll { selection.contains($0.id) }
selection.removeAll()
}
}
On macOS, thanks to .onMove, the user can simply click, hold, and drag a row to reorder it natively.
7. Best Practices and Performance
When working with advanced Swift programming and mutable lists, keep the following in mind:
- Stable Identifiers: Make sure your
UUIDdoes not regenerate on every view render. If you use a dynamically calculatedidinstead of a constant property (let id = UUID()), the delete and move animations will be erratic. - Custom Animations: If you modify the array outside the
.onDeleteor.onMovemodifiers (e.g., a “Delete All” button), be sure to wrap the state change in awithAnimation { }block so SwiftUI transitions the UI smoothly. - Centralized State Management: For more complex applications, do not keep the array directly in the View with
@State. Move your logic to a ViewModel using@Observable(orObservableObjectin older versions) to separate business logic from the interface.
Conclusion
Knowing how to implement editable lists in SwiftUI is a prerequisite for any iOS Developer. We’ve seen how, by using Xcode and Swift, we can reduce the architectural complexity of UIKit down to a couple of declarative modifiers: .onDelete and .onMove.
Furthermore, by adopting a cross-platform approach, we’ve adapted the interactions so they feel native both in a touch environment like iOS and watchOS, and in a desktop environment with macOS.
If you have any questions about this article, please contact me and I will be happy to help you . You can contact me on my X profile or on my Instagram profile.