28

Perhaps I'm particularly dense this morning but I'm trying to delete a row from a List in SwiftUI on macOS.

The issue is that there is no UI exposed to perform the delete. By that I mean that the List does not respond to delete key presses, has no right click menu nor supports any other gestures like swipe to delete (which would be odd on macOS anyway).

Here's the example I'm using:

import SwiftUI

struct ContentView: View {
    @State var items = ["foo", "bar", "baz"]
    @State var selection: String? = nil

    var body: some View {
        List(selection: $selection) {
            ForEach(items, id: \.self) { Text($0) }
                .onDelete { self.items.remove(atOffsets: $0)}
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

The exact same code on iOS gives me a table view with the standard "swipe left to delete" UI. On macOS there is just nothing.

I tried adding

    .onDeleteCommand(perform: {
        if let sel = self.selection, let idx = self.items.firstIndex(of: sel) {
            self.items.remove(at: idx)
        }
    })

to the List but still no response to delete key presses.

How does one enable List row deletion on macOS?

4
  • I assume you have to either add Edit mode explicitly or swipe a list row to the left side. Commented Jan 16, 2020 at 8:23
  • Mmm, I've tried swiping and adding .environment(\.editMode, .constant(.active)) gives me 'editMode' is unavailable in macOS Commented Jan 16, 2020 at 8:29
  • 1
    Yes, just check there is no EditMode, but swipe just works - shows red Delete button at right side and on click removes record. Commented Jan 16, 2020 at 8:33
  • Oh, I see. I was mouse dragging to simulate the swipe but the magic mouse gesture works. There has to be a way to get backspace to work though? I would have expected that to be the default. Commented Jan 16, 2020 at 8:36

4 Answers 4

23

For macOS, we use right-click and see menu options like Delete. You can add to your list a context menu like

List(selection: $selection) {
    ForEach(items, id: \.self) { item in
        Text(item)
            .contextMenu {
                Button(action: {
                    // delete item in items array
                }){
                    Text("Delete")
                }
            }
    }

}

Sign up to request clarification or add additional context in comments.

Comments

14

This code enables the "Delete" menu and removes the selected item when I select Edit > Delete (without manually wiring up the menu):

struct ContentView: View {
    @State var items = ["foo", "bar", "baz"]
    @State var selection: String? = nil

    var body: some View {
        List(selection: $selection) {
            ForEach(items, id: \.self) { Text($0) }
        }
        .onDeleteCommand {
            if
                let sel = self.selection,
                let idx = self.items.firstIndex(of: sel) {
                print("delete item: \(sel)")
                self.items.remove(at: idx)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .animation(.default)
    }
}

Then, to make the delete key work, make it the keyboard equivalent for the Delete menu option: - Edit Main.storyboard - Select Edit > Delete - Click in the "Key Equivalent" field - Hit the delete key.

enter image description here

Run the app, select an item, hit the delete key, and your item should disappear.

2 Comments

That looks equivalent to my initial question. Do the two versions behave differently for you?
Hmm, I think I came full circle to a variant of your original code... But, just edited my answer to add how to hook up the Delete key to the Delete menu item. With that in place, I can click a row and hit the delete key and it deletes. So I guess the short answer is "add the delete key as the keyboard equivalent to the Edit > Delete menu in Menu.storyboard", but I'll leave the assembled code in place for easy copy/pasting.
2

I am facing the same issue, and I found out two fingers swipe triggering deletion. It crashes with the fatal error since I delete element in onDelete(), and I'm still figuring out how to fix it:

index out of range

Edit: I replaced index to element, and it works fine.

ForEach(elementArray.indices) { index in 
    // code... 
}

to

ForEach(elementArray) { element in 
    // code... 
}

and it works fine :)

1 Comment

Indeed, two finger swiping works on MacOS. I was attempting to click and drag to perform the swipe, which obviously didn't work.
1

I've found a quite convoluted solution to this and I hope there's a better way.

What I did is the following:

  • Wire up an action to the existing "Delete" command
  • Create an "ObservableObject" Menu that publishes selected menu commands
  • Pass the publisher through to the ContentView so it can subscribe and act on changes

Here are the two relevant files:

First, AppDelegate.swift:

enum MenuCommand {
    case none
    case delete
}

class Menu: ObservableObject {
    @Published var item: MenuCommand = .none
}


@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var window: NSWindow!
    @ObservedObject var menu = Menu()

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView(menu: menu)

        // Create the window and set the content view. 
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: contentView)
        window.makeKeyAndOrderFront(nil)
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }

    @IBAction func delete(_ sender: Any) {
        print("delete menu")
        menu.item = .delete
    }

}

and ContentView.swift:

import SwiftUI

struct ContentView: View {
    @ObservedObject var menu: Menu
    @State var items = ["foo", "bar", "baz"]
    @State var selection: String? = nil

    var body: some View {
        List(selection: $selection) {
            ForEach(items, id: \.self) { Text($0) }
        }
        .onReceive(
            self.menu.objectWillChange
                .receive(on: RunLoop.main)) { _ in
            if
                case .delete = self.menu.item,
                let sel = self.selection,
                let idx = self.items.firstIndex(of: sel) {
                print("delete item: \(sel)")
                self.items.remove(at: idx)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .animation(.default)
    }
}

NB: don't forget to connect the "Delete" menu item to the IBAction.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.