How to add confirmation to .ondelete() of List in SwiftUI
Asked Answered
T

5

16

How can I add a confirmation to my deleteRow() function? A already added the .alert but I don't know how to continue.

List {
    ForEach(data, id: \.self) { item in
        Text(item)
        .alert(isPresented: self.$showingDeleteAlert) {
            Alert(title: Text("..."), message: Text("..."), primaryButton: .destructive(Text("Delete")) {
                deleteRow // ???
                }, secondaryButton: .cancel()
            )
        }
    }
    .onDelete(perform: deleteRow)
}

func deleteRow(at indexSet: IndexSet) {
    for index in indexSet {
        let item = data[index]
        viewContext.delete(item)
        do {
            try viewContext.save()
        } catch let error {
            print("Error: \(error)")
        }
    }
}
``
Tannie answered 3/7, 2020 at 18:1 Comment(0)
S
7

Here is possible approach:

@State private var toBeDeleted: IndexSet?

...

List {
    ForEach(data, id: \.self) { item in
        Text(item)
        .alert(isPresented: self.$showingDeleteAlert) {
            Alert(title: Text("..."), message: Text("..."), primaryButton: .destructive(Text("Delete")) {
                    for index in self.toBeDeleted {
                        let item = data[index]
                        viewContext.delete(item)
                        do {
                            try viewContext.save()
                        } catch let error {
                            print("Error: \(error)")
                        }
                    }
                    self.toBeDeleted = nil
                }, secondaryButton: .cancel() {
                    self.toBeDeleted = nil
                }
            )
        }
    }
    .onDelete(perform: deleteRow)
}

func deleteRow(at indexSet: IndexSet) {
    self.toBeDeleted = indexSet           // store rows for delete
    self.showingDeleteAlert = true
}
Soda answered 3/7, 2020 at 18:10 Comment(3)
Is it also possible to do this with List(selection: $selection) If i do this and add a custom delete button to show an alert before deletion the $selection binding becomes empty.Micromho
I'm getting: For-in loop requires 'IndexSet?' to conform to 'Sequence'; did you mean to unwrap optional?Chiarra
But onDelete immediately performs UI changes. When you show your alert, the row will be already deleted (in UI, not in data source), so with this approach we need to somehow restore that row on cancellation I guess…Permatron
L
22

In iOS 15+ you can use swipe actions and confirmationDialog

@State private var showingDeleteAlert = false

List {
    ForEach(data, id: \.self) { item in
        Text(item)
        .swipeActions {
            Button("Delete", role: .destructive) {
                showingDeleteAlert = true
            }
        }
        .confirmationDialog(
            Text("..."),
            isPresented: $showingDeleteAlert,
            titleVisibility: .visible
        ) {
             Button("Delete", role: .destructive) {
             withAnimation {
                deleteItem(item)
             }
        }
    }
}

func deleteItem(_ item: NSManagedObject) {
        viewContext.delete(item)
        do {
            try viewContext.save()
        } catch let error {
            print("Error: \(error)")
        }
    }
}
Lamkin answered 25/3, 2022 at 20:24 Comment(3)
on iPhone the confirmation dialogue gets dismissed before it can be tapped. Are you sure this works? I am testing on iOS 16. There seems to be a problem only with role:. destructiveShana
Workaround is don't set the swipe action button's role to be destructive (needs to be non-destructive). Instead set the swipe action's button's tint color to red. I suppose having it non-destructive makes sense, because destructive deletes it from UI even before confirmation. Since we are only asking for confirmation, technically it is non-destructive (we can set it to red)Shana
Great solution, but .confirmationDialog( should be a little lower, on ForEach level. This will prevent confirmationDialog from showing up when not needed after we delete an item.Lustring
S
7

Here is possible approach:

@State private var toBeDeleted: IndexSet?

...

List {
    ForEach(data, id: \.self) { item in
        Text(item)
        .alert(isPresented: self.$showingDeleteAlert) {
            Alert(title: Text("..."), message: Text("..."), primaryButton: .destructive(Text("Delete")) {
                    for index in self.toBeDeleted {
                        let item = data[index]
                        viewContext.delete(item)
                        do {
                            try viewContext.save()
                        } catch let error {
                            print("Error: \(error)")
                        }
                    }
                    self.toBeDeleted = nil
                }, secondaryButton: .cancel() {
                    self.toBeDeleted = nil
                }
            )
        }
    }
    .onDelete(perform: deleteRow)
}

func deleteRow(at indexSet: IndexSet) {
    self.toBeDeleted = indexSet           // store rows for delete
    self.showingDeleteAlert = true
}
Soda answered 3/7, 2020 at 18:10 Comment(3)
Is it also possible to do this with List(selection: $selection) If i do this and add a custom delete button to show an alert before deletion the $selection binding becomes empty.Micromho
I'm getting: For-in loop requires 'IndexSet?' to conform to 'Sequence'; did you mean to unwrap optional?Chiarra
But onDelete immediately performs UI changes. When you show your alert, the row will be already deleted (in UI, not in data source), so with this approach we need to somehow restore that row on cancellation I guess…Permatron
F
4

Continuing the answer by @vadian and the comments to his answer, this code works for me with minimal glitching. Basically, move the confirmation dialog to a lower level, but in order to keep the reference to the item being deleted, add a @State var to track it.

Note: The reason I say "minimal glitching", is that when you complete the swipe to delete, the row does actually disappear and instantly reappears in the same spot. It's almost imperceptible without using slow animations in the simulator. But it's the least glitchy solution I've found so far. If anyone can figure out a way to have it perfect, I'm all ears.

@State private var showingDeleteAlert = false
@State private var itemToDelete: Item? = nil

List {
    ForEach(data, id: \.self) { item in
        Text(item)
        .swipeActions {
            Button("Delete", role: .destructive) {
                self.itemToDelete = item
                showingDeleteAlert = true
            }
        }
    }
    .confirmationDialog(
        Text("..."),
        isPresented: $showingDeleteAlert,
        titleVisibility: .visible
    ) {
         Button("Delete", role: .destructive) {
         withAnimation {
            deleteItem(itemToDelete)
         }
    }
}

func deleteItem(_ item: NSManagedObject?) {
        guard let item else { return }

        viewContext.delete(item)
        do {
            try viewContext.save()
        } catch let error {
            print("Error: \(error)")
        }
    }
}
Filly answered 9/1, 2023 at 16:9 Comment(0)
S
2

Here is my take, unlike above it will not momentarily delete the item, but when it does delete, the smooth delete animation is unfortunately not present.

This solution uses a normal swipe action (non-destructive) colored red with a "trash" icon and an alert instead of a confirmation dialog.

@State private var showingDeleteAlert = false
@State private var itemToDelete: Item? = nil

List {
    ForEach(data, id: \.self) { item in
        Text(item)
        .swipeActions {
             Button {
                 self.itemToDelete = event
                 showingDeleteAlert = true
             } label: {
                 Label("Delete", systemImage: "trash")
             }
         }
         .tint(.red)
    }
    .alert("Confirm?", isPresented: $showingDeleteAlert, actions: {
        Button("Delete", role: .destructive, action: {
             deleteItem(itemToDelete)               
        })
    })
}

func deleteItem(_ item: NSManagedObject?) {
    guard let item else { return }
    viewContext.delete(item)
    do {
        try viewContext.save()
    } catch let error {
        print("Error: \(error)")
    }
}
Spume answered 30/12, 2023 at 17:21 Comment(0)
R
0

I've gotten around animation issues by using a ViewModifier to encapsulate not only the alert, but also the swipe action with the state variable. This cleans up the call site considerably.

Note that if you change the SwipeAction button role to destructive as that role will delete the item from the list, removing the alert from the view. So I manually tinted the button red to get around this issue.

extension View {
    func deleteSwipeActionAlert(title: LocalizedStringKey, message: LocalizedStringKey, action: @escaping () -> ()) -> some View {
        modifier(DeleteSwipeActionAlertModifier(title: title, message: message, action: action))
    }
}

struct DeleteSwipeActionAlertModifier : ViewModifier {
    let title: LocalizedStringKey
    let message: LocalizedStringKey
    let action: () -> ()
    
    @State private var isPresented: Bool = false
    
    func body(content: Content) -> some View {
        content
            .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                Button {
                    isPresented = true
                } label: {
                    Label("Delete", systemImage: "trash")
                }
                .tint(.red)
            }
        
            .alert(title, isPresented: $isPresented) {
                Button("Delete", role: .destructive, action: action)
                Button("Cancel", role: .cancel) { }
            } message: { Text(message) }
    }
}

And an example of implementation:

struct TestList : View {
    @State private var items = ["Item 1", "Item 2", "Item 3"]
    
    var body: some View {
        List($items, id: \.self) { item in
            Text("List entry for [\(item.wrappedValue)]")
                .deleteSwipeActionAlert(title: "Confirm delete", message: "Are you sure you want to delete?") {
                    items.remove(at: items.firstIndex(of: item.wrappedValue)!)
                    print("Do something to delete the record here.")
                }
        }
    }
}
Renferd answered 13/6 at 16:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.