swiftuiswiftui-listswiftui-swipeactions

How to Keep SwipeActions Open in SwiftUI?


I'm trying to implement a delete function in my SwiftUI app using swipeActions. When a user swipes on a list item, they should see a delete button. Tapping this button should display a confirmation alert before deleting the item.

However, I'm facing an issue where the swipe actions automatically close as soon as the alert appears. This causes the list item to slide back to its original position, making it unclear which item is about to be deleted.

I want the swipe actions to remain open while the alert is displayed, so the user can clearly see which item they're confirming to delete.

Here's the minimal code demonstrating the problem:

import SwiftUI

struct ContentView: View {
    @State private var numbers = Array(1...10)
    @State private var showingDeleteAlert = false
    @State private var itemToDelete: Int?
    
    var body: some View {
        List {
            ForEach(numbers, id: \.self) { number in
                Text("\(number)")
                    .swipeActions {
                        Button(role: .none) {
                            itemToDelete = number
                            showingDeleteAlert = true
                        } label: {
                            Label("Delete", systemImage: "trash")
                        }
                        .tint(.red)
                    }
            }
        }
        .alert("Confirm", isPresented: $showingDeleteAlert) {
            Button("Cancel", role: .cancel) {}
            Button("Delete", role: .destructive) {
                deleteItem()
            }
        } message: {
            Text("Are you sure you want to delete this item?")
        }
    }
    
    private func deleteItem() {
        if let index = numbers.firstIndex(of: itemToDelete ?? 0) {
            numbers.remove(at: index)
        }
        itemToDelete = nil
    }
}

Issue:

Question:

For reference, I have attached a GIF illustrating the desired behavior.

enter image description here

Any guidance or solutions would be greatly appreciated!


Solution

  • Unlike Menu and Picker that can use .menuActionDismissBehavior to keep the menu open after a selection, I am not aware of a similar modifier that works with .swipeActions.

    However, note that when the swipe buttons are revealed, the content is shifted out of view and is no longer visible. So your goal of keeping the buttons visible "so the user can clearly see which item they're confirming to delete" becomes moot, since the item wouldn't be visible at all (depending on the type and length of the content). This is obvious in your GIF and code example.

    As such, you may want to take another approach that would allow the user to clearly confirm which item they're deleting by including the item details in the alert/confirmation message.

    You can do this by using .alert with the presenting parameter. Since you're already setting the itemToDelete, this is really simple:

    .alert("Confirm", isPresented: $showingDeleteAlert, presenting: itemToDelete) { item in
                Button("Cancel", role: .cancel) {}
                Button("Delete", role: .destructive) {
                    deleteItem()
                }
            } message: { item in
                Text("Are you sure you want to delete item \(item)?")
            }
    

    However, to avoid potential racing issues between the setting of the item and the presentation of the alert, you may want to display the alert as the result of setting the item to delete, rather than immediately toggling the showingDeleteAlert after setting the itemToDelete.

    Here's the full code:

    import SwiftUI
    
    struct SwipeActionDeleteConfirm: View {
        @State private var numbers = Array(1...10)
        @State private var showingDeleteAlert = false
        @State private var itemToDelete: Int?
        
        var body: some View {
            List {
                ForEach(numbers, id: \.self) { number in
                    Text("\(number)")
                        .swipeActions {
                            Button {
                                itemToDelete = number
                                // showingDeleteAlert = true // <- remove this line
                            } label: {
                                Label("Delete", systemImage: "trash")
                            }
                            .tint(.red)
                        }
                }
            }
            .onChange(of: itemToDelete){
                if itemToDelete != nil {
                    showingDeleteAlert = true // <- show alert when item changes and is not nil
                }
            }
            
            //Use .alert with presenting parameter that provides an action closure parameter (item)
            .alert("Confirm", isPresented: $showingDeleteAlert, presenting: itemToDelete) { item in // <- note the closure parameter
                Button("Cancel", role: .cancel) {
                    itemToDelete = nil // <- reset itemToDelete on cancel
                }
                Button("Delete", role: .destructive) {
                    withAnimation {
                        deleteItem() // <- optional - delete with animation for a smoother update of the list
                    }
                }
            } message: { item in // <- note the closure parameter
                Text("Are you sure you want to delete item \(item)?") // <- include the item details/name
            }
        }
        
        private func deleteItem() {
            if let index = numbers.firstIndex(of: itemToDelete ?? 0) {
                    numbers.remove(at: index)
            }
            itemToDelete = nil
        }
    }
    
    #Preview {
        SwipeActionDeleteConfirm()
    }
    

    enter image description here