Removing Items from SwiftUI Lists in Mac Apps
Most examples of removing items from SwiftUI lists use the .onDelete
handler, which is not available for Mac apps. In this article I share what I learned to remove list items from SwiftUI Mac apps.
To remove items from SwiftUI lists in Mac apps, you must perform the following tasks:
- Add a variable to the list view to store the selected list item.
- If you are using a navigation link, supply a tag and selection when creating the link.
- Make your struct or class conform to the
Equatable
andHashable
protocols. - Add the
.onDeleteCommand
handler to the list. The.onDeleteCommand
handler is the handler SwiftUI Mac apps use to remove list items.
Add a Selection Variable to the List View
To remove an item from a SwiftUI list, the list view requires a variable to store the item you want to remove. Create an optional for the variable and set it to nil initially.
@State private var selection: ListItemStruct? = nil
ListItemStruct
is the name of the data structure in your app that you want to show in the list.
When you supply this selection when creating a navigation link, SwiftUI keeps track of the selected item in the list.
Supply a Tag and Selection to the Navigation Link
Most SwiftUI apps that use lists use a navigation link to create master-detail interfaces. Select an item from the list to show additional information in the detail view. Add a call to NavigationLink
in the master view and set its destination to the detail view.
NavigationLink(destination: DetailView(detailItem: item)) {
...
}
To support more complex selection behavior, you must supply two additional arguments to the navigation link call: tag
and selection
. Usually the tag is the current list item you’re adding to the list. The selection is the variable you added to the list view.
The following code demonstrates how to show a list of a book’s chapters:
// The book has an array of chapters.
@Binding var book: Book
@State private var selection: Chapter? = nil
List($book.chapters) { $chapter in
// ChapterView is a SwiftUI view that
// displays the chapter's contents.
NavigationLink(destination: ChapterView(chapter: chapter),
tag: chapter,
selection: $selection) {
TextField("", text: $chapter.title)
}
}
This example uses the improved syntax Apple added in Xcode 13 to bind list text fields to items in an array. If you’re using Xcode 12 you will have to use a Text
to display the titles and remove the $ character from $chapter
.
Make Your Struct/Class Conform to Equatable and Hashable Protocols
To use the tag
and selection
arguments in a navigation link, your struct or class must conform to the Equatable
and Hashable
protocols. Your project won’t compile until you make the struct or class conform to those protocols.
Making your struct or class conform to Equatable
requires you to implement the ==
operator to check for equality.
static func == (lhs: Chapter, rhs: Chapter) -> Bool {
lhs.id == rhs.id
}
Replace Chapter
with the name of your struct or class. Do whatever comparisons you need to make to determine that two objects are equal. SwiftUI list items require a unique ID. That’s what I used to determine equality in the example.
To conform to the Hashable
protocol, you must implement the hash
function.
func hash(into hasher: inout Hasher)
Inside the hash
function, call the hasher’s combine
function for each property in your struct or class. Supply the name of the property.
func hash(into hasher: inout Hasher) {
hasher.combine(id)
// Call hasher.combine() for each property
// in your data structure.
}
Add the .onDeleteCommand Handler to the List
The last step is to remove the item from the list. Add the .onDeleteCommand
handler to the list to enable the Delete menu item in the Edit menu. Inside the block of code, you will use the selection variable you added to the list view to find the selection index. Use the selection index to remove the item from the list.
.onDeleteCommand {
if let selection = self.selection,
let selectionIndex = book.chapters.firstIndex(of: selection) {
book.chapters.remove(at: selectionIndex)
}
}
The firstIndex
function returns the first selected item in the list. After getting the index of the selected item, remove that item from the array that populates the list.
Now let’s put the whole list view together.
List($book.chapters) { $chapter in
NavigationLink(destination: ChapterView(chapter: chapter),
tag: chapter,
selection: $selection) {
TextField("", text: $chapter.title)
}
}
.onDeleteCommand {
if let selection = self.selection,
let selectionIndex = book.chapters.
firstIndex(of: selection) {
book.chapters.remove(at: selectionIndex)
}
}
Removing an Item with a Button and the Delete Key
At this point you can remove a list item by choosing Edit > Delete. But you may want to provide a button to remove an item. How do you remove list items by clicking a button?
Start by moving the code inside the .onDeleteCommand
handler into its own function. Moving the deletion code into a separate function will also make the list code cleaner.
func deleteChapter() {
if let selection = self.selection,
let selectionIndex = book.chapters.
firstIndex(of: selection) {
book.chapters.remove(at: selectionIndex)
}
}
Now the .onDeleteCommand
handler looks like the following:
.onDeleteCommand {
deleteChapter()
}
Call the function in your button. Add a .keyboardShortcut
handler to the button to remove list items with the Delete (Backspace) key.
Button(action: {
deleteChapter()
}, label: {
Label("Delete", systemImage: "trash")
.foregroundColor(.red)
})
.keyboardShortcut(.delete, modifiers: [])
I haven’t figured out how to add a keyboard shortcut for the Delete menu item in the Edit menu. Every example I’ve seen on creating keyboard shortcuts for menu items in SwiftUI uses custom menu items, not the menu items that Apple supplies. I’ll update the article if I ever find a solution.
Sample Project
I have a project on GitHub that supports removing items from lists. Look at the PageListView.swift
and Wiki.swift
files for the list removal code.
Another sample project for SwiftUI list item removal is the Feed Read project by TrozWare for the Back to the Mac conference.