Swift and SwiftUI tutorials for Swift Developers

EditButton in SwiftUI

Any iOS Developer who has been around for a while remembers the days of UITableView in UIKit. Configuring the editing mode required dealing with delegates, data sources, methods like commit editingStyle, and manually managing the UI state. It was powerful, yes, but also error-prone and verbose.

With the arrival of modern Swift programming and declarative paradigms, Apple changed the game. Today, enabling list editing is as simple as using an EditButton in SwiftUI.

However, behind that apparent simplicity lies an Environment-based system that can sometimes confuse even experienced developers. In this extensive tutorial, we will explore in depth what it is, how it works under the hood, and how to implement the EditButton in your Swift projects, covering the specifics of Xcode when developing for iOS, macOS, and watchOS.


1. What is the EditButton in SwiftUI?

The EditButton in SwiftUI is a pre-configured view provided by Apple’s framework that automatically toggles the edit state of the current view.

In simple terms, it’s that classic button that usually appears in the top right corner of a navigation bar. When tapped, it changes its text from “Edit” to “Done”, and places compatible views within its hierarchy (usually a List component) into edit mode.

This edit mode reveals hidden interface controls, such as:

  • Deletion controls: The iconic red circles with a minus sign.
  • Reordering controls: The three horizontal lines (hamburger) on the right side of the cell.
  • Multiple selection controls: Checkboxes to select several items at once.

But the button alone doesn’t do magic. It works in conjunction with list modifiers to transform the user experience.


2. Basic Implementation in iOS

For an iOS Developer, the most common implementation of an EditButton in SwiftUI is inside a list of dynamic items. Let’s build a simple app to manage an inventory.

Open Xcode, create a new SwiftUI project, and take a look at this code:

import SwiftUI

struct InventoryView: View {
    @State private var items = ["MacBook Pro", "iPhone 15", "Apple Watch Ultra", "AirPods Pro"]
    
    var body: some View {
        NavigationView {
            List {
                ForEach(items, id: \.self) { item in
                    Text(item)
                }
                .onDelete(perform: deleteItem)
                .onMove(perform: moveItem)
            }
            .navigationTitle("Inventory")
            .toolbar {
                EditButton() // Here is the magic!
            }
        }
    }
    
    // Function to delete items
    func deleteItem(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }
    
    // Function to reorder items
    func moveItem(from source: IndexSet, to destination: Int) {
        items.move(fromOffsets: source, toOffset: destination)
    }
}

Analyzing the Code:

  1. ForEach inside List: It is vital to understand that the .onDelete and .onMove modifiers are applied to the ForEach, not the List directly. This is because you might have static sections in your list that you don’t want to be editable.
  2. EditButton(): We place it inside the .toolbar modifier. SwiftUI is smart enough to position it in the standard OS location (top right on iOS).
  3. The Magic: Just by including EditButton(), SwiftUI automatically connects the button to the list. When pressed, the delete and reorder controls will appear.

3. Under the Hood: The Environment and EditMode

To master Swift programming in declarative interfaces, copying and pasting isn’t enough. You must understand why it works.

The EditButton in SwiftUI does not have a direct reference to the List. Variables are not passed between them. Instead, the button modifies an Environment Value called editMode.

The editMode is an optional Binding<EditMode> that flows down the view hierarchy. The possible values for EditMode are:

  • .inactive: The normal state.
  • .active: The list is in edit mode.
  • .transient: A temporary state (rarely used directly).

3.1. Reading the Edit State

Sometimes, as an iOS Developer, you need to know if the view is in edit mode to hide or show other elements (for example, hiding an “Add New” button while editing). You can read this environment value like this:

import SwiftUI

struct ListWithStateView: View {
    @State private var items = ["Apples", "Pears", "Oranges"]
    // We read the environment value
    @Environment(\.editMode) private var editMode
    
    var body: some View {
        NavigationView {
            VStack {
                if editMode?.wrappedValue.isEditing == true {
                    Text("You are in edit mode!")
                        .foregroundColor(.red)
                        .padding()
                }
                
                List {
                    ForEach(items, id: \.self) { item in
                        Text(item)
                    }
                    .onDelete { items.remove(atOffsets: $0) }
                }
            }
            .navigationTitle("Fruits")
            .toolbar {
                EditButton()
            }
        }
    }
}

3.2. Activating Edit Mode Without the Default EditButton

If your app’s design requires a custom button (perhaps a floating pencil icon instead of the standard text in the navigation bar), you can inject and modify the state yourself.

Note: Modifying the editMode manually can be a bit verbose in SwiftUI because it requires injecting a local @State into the .environment.

struct CustomEditView: View {
    @State private var items = ["Swift", "Kotlin", "Python"]
    @State private var isEditing: EditMode = .inactive // Our local state
    
    var body: some View {
        NavigationView {
            List {
                ForEach(items, id: \.self) { item in
                    Text(item)
                }
                .onDelete { items.remove(atOffsets: $0) }
            }
            .navigationTitle("Languages")
            // We overwrite the environment with our local state
            .environment(\.editMode, $isEditing)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: {
                        withAnimation {
                            isEditing = isEditing == .active ? .inactive : .active
                        }
                    }) {
                        Image(systemName: isEditing == .active ? "checkmark.circle.fill" : "pencil.circle")
                            .font(.title2)
                    }
                }
            }
        }
    }
}

4. Multiple Item Selection

Another fantastic use case for the EditButton in SwiftUI is multiple selection. Imagine the Mail app, where you tap “Edit” and circles appear to the left of each email to select several and delete them at once.

To achieve this, you simply pass a Binding to a Set property in the List initializer.

struct MultiSelectView: View {
    @State private var items = ["Task 1", "Task 2", "Task 3", "Task 4"]
    // Set to store the IDs of selected items
    @State private var selection = Set<String>()
    
    var body: some View {
        NavigationView {
            VStack {
                // We pass the 'selection' binding to the list
                List(selection: $selection) {
                    ForEach(items, id: \.self) { item in
                        Text(item)
                    }
                }
                .navigationTitle("Tasks")
                .toolbar {
                    EditButton()
                }
                
                if !selection.isEmpty {
                    Button(role: .destructive, action: deleteSelected) {
                        Text("Delete \(selection.count) tasks")
                            .frame(maxWidth: .infinity)
                            .padding()
                            .background(Color.red.opacity(0.1))
                            .cornerRadius(10)
                    }
                    .padding()
                }
            }
        }
    }
    
    func deleteSelected() {
        items.removeAll { selection.contains($0) }
        // Clear the selection after deleting
        selection.removeAll()
    }
}

When you tap the EditButton, SwiftUI will automatically change the interface to show checkboxes instead of individual delete buttons.


5. The EditButton in a Multiplatform Environment

SwiftUI promises “Learn once, apply anywhere”. However, a good iOS Developer knows that user interface conventions vary wildly between iOS, macOS, and watchOS. Let’s see how the EditButton behaves in each environment using Xcode.

5.1. iOS and iPadOS

This is the natural habitat of the EditButton. It works exactly as we have described throughout the tutorial. It integrates perfectly into navigation bars, supports swipe-to-delete gestures even without being in edit mode, and the animations are fluid.

5.2. macOS

Here is where reality hits hard. On macOS, there is no traditional concept of an EditButton for lists.

Mac users don’t expect to click an “Edit” button to delete an item from a list. They expect to:

  1. Click on an item to select it and press the Backspace or Delete key.
  2. Right-click and select “Delete” from a context menu.

If you try to compile an EditButton() in a macOS target in Xcode, it simply won’t do anything useful or might not even show up in a standard way. On macOS, deletion is best handled by adding a context menu and keyboard commands:

// Example adapted for macOS
List(selection: $selection) {
    ForEach(items, id: \.self) { item in
        Text(item)
            .contextMenu {
                Button("Delete") {
                    // Deletion logic
                }
            }
    }
}
.onDeleteCommand {
    // Executes when pressing the Delete key on Mac
    deleteSelected()
}

5.3. watchOS

On the Apple Watch, space is vital. The EditButton is indeed available on watchOS, but it requires special care.

Generally, lists on watchOS prefer swipe actions. However, if you need a bulk edit mode or reordering, you can use it. Instead of a top navigation bar (which barely exists on watchOS), the button is usually placed in the toolbar (.toolbar) and the system will decide the best place to show it, often requiring the user to scroll up.


6. Integration with Core Data and SwiftData

When working with data persistence, deleting an item from a simple array of Strings is not enough. You must ensure you delete the actual object from the database.

If you use the powerful new SwiftData framework alongside SwiftUI, .onDelete works gloriously and cleanly.

import SwiftUI
import SwiftData

struct DatabaseView: View {
    // We get the SwiftData context
    @Environment(\.modelContext) private var context
    // We query the data
    @Query private var notes: [Note]
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(notes) { note in
                    Text(note.title)
                }
                .onDelete(perform: deleteNote)
            }
            .navigationTitle("My Notes")
            .toolbar {
                EditButton()
            }
        }
    }
    
    func deleteNote(at offsets: IndexSet) {
        for index in offsets {
            let noteToDelete = notes[index]
            context.delete(noteToDelete) // We delete from the database
        }
        // Note: You don't need to save explicitly, SwiftData autosaves
    }
}

Here we see the true power of Swift programming: concise, readable, and extremely efficient code.


Conclusion

The EditButton in SwiftUI is much more than a simple button; it is the gateway to an entire list manipulation subsystem integrated into the SwiftUI environment. Gone are the days of implementing five different delegate methods in Xcode just to allow a user to delete a row.

Understanding how the Environment handles editMode, knowing when to use multiple selection methods, and above all, understanding the philosophical interface differences between iOS, macOS, and watchOS, will separate you from a beginner programmer and solidify you as a versatile and professional iOS Developer.

Swift programming continues to evolve to make the developer’s intentions translate into code as quickly as possible. Master these tools and your applications will not only be more robust, but much more enjoyable to build.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

@discardableResult in Swift

Next Article

GlassEffectContainer in SwiftUI

Related Posts