SwiftUI `swipeActions` and `confirmationDialog` delete the wrong item

Viewed 593

I am trying to use iOS 15.0 swipeActions and confirmationDialog to delete an item in a List.

But what happens is that the wrong item gets deleted.

Here is my code:

struct ConversationsSection: View {

@State private var isShowingDeleteActions = false

let items = ["One", "Two", "Three", "Four", "Five"]

var body: some View {
    List(items, id: \.self) { item in
        Text(item)
            .swipeActions(edge: .trailing) {
                Button(role: .destructive) {
                    isShowingDeleteActions = true
                    print("Trying to delete: " + item)
                } label: {
                    Label("Delete", systemImage: "trash")
                }
            }
            .confirmationDialog("Delete item?", isPresented: $isShowingDeleteActions) {
                Button("Confirm Delete", role: .destructive) {
                    print("Actually deleting: " + item)
                    isShowingDeleteActions = false
                }
            }
    }
}

}

The output is:

Trying to delete: Two
Actually deleting: Four
Trying to delete: Five
Actually deleting: Three

So I swipe an item and confirmationDialog is presented. But inside confirmationDialog another item is passed. Why is that?

2 Answers

I think of it this way: you have a confirmationDialog modifier inside your ForEach loop, so there are multiple confirmation dialogs whose presentation is controlled by a single $isShowingDeleteActions state variable. When that happens, SwiftUI can't reliably show the dialog from the instance of the loop that sets the state variable – so it might end up showing a different dialog, and one whose item value is different.

I get how frustrating it is!

One workaround would be to move the confirmationDialog out of the loop altogether, so there'll only ever be one modifier using $isShowingDeleteActions. The snag there is that there's no longer a direct reference to item, but we could compensate by keeping a reference in a second state variable:

struct ConversationsSection: View {

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

var body: some View {
    List(items, id: \.self) { item in
        Text(item)
            .swipeActions(edge: .trailing) {
                Button(role: .destructive) {
                    itemToDelete = item
                    isShowingDeleteActions = true
                } label: {
                    Label("Delete", systemImage: "trash")
                }
            }
    }
    .confirmationDialog("Delete item?", isPresented: $isShowingDeleteActions) {
        Button("Confirm Delete", role: .destructive) {
            if let item = itemToDelete {
                print("Actually deleting: " + item)
                isShowingDeleteActions = false
                itemToDelete = nil
            }
        }
    }
}

The solution from Scott works fine, but when you want to show the confirmationDialog on an iPad, the attachment is off (as it is now using the List as attachment). See the screenshot below:

iPad screenshot of attachment problem

You can fix this by extracting the Text together with its modifiers and the @State property into a new view like follows:

import SwiftUI

struct ItemView: View {
    
    @State private var isShowingDeleteActions = false
    
    var item: String
    
    var body: some View {
        Text(item)
            .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
            .swipeActions(edge: .trailing) {
                Button(role: .destructive) {
                    isShowingDeleteActions = true
                    print("Trying to delete: " + item)
                } label: {
                    Label("Delete", systemImage: "trash")
                }
            }
            .confirmationDialog("Delete item?", isPresented: $isShowingDeleteActions) {
                Button("Confirm Delete", role: .destructive) {
                    print("Actually deleting: " + item)
                    isShowingDeleteActions = false
                }
            }
    }
}

struct ConversationsSection: View {

    let items = ["One", "Two", "Three", "Four", "Five"]

    var body: some View {
        NavigationView {
            List(items, id: \.self) { item in
                ItemView(item: item)
            }
        }
    }
}

Screenshot of this:

iPad screenshot, where the attachment is correct

Related