UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Alert before onDelete list element

Forums > SwiftUI

I'm not able to trigger an Alert View from the onDelete closure, invoked deleting a list element, for example:

    private var viewUsers: some View {
        NavigationView {
            List {
                ForEach (self.usersViewModel.listOfUsers) {user in
                    VStack(alignment: .leading){
                        HStack {
                            Text(user.nomeUtente.capitalized).bold()
                            Text(user.cognomeUtente.capitalized).bold()
                        }
                        Text(user.mailUtente).foregroundColor(.gray)
                    }
                }
                .onDelete{ indexSet in
                    DispatchQueue.main.async() {
                        Alert(title: Text("title"), message: Text("message"), primaryButton: .default(Text("primaryButtonLabel")), secondaryButton: .cancel(Text("secondaryButtonLabel")))
                    }
                }
            }
            .navigationBarTitle(Text("Users"), displayMode: .inline)
            .navigationBarItems(trailing: EditButton())
        }
    }

Any suggestion ?

Thanks in advance.

2      

Hi @giurobrossi, I think that a. views cannot be inserted via the .onDelete method in general (only logic) and b. that you need to introduce alerts through the .alert modifier, like so:

.alert(isPresented: $presentAlert) {
      Alert(...)
}

That would require you to introduce a presentAlert flag that is toggled to on in your .onDelete closure and toggled back off in the alert's handler.

2      

That's fine, but the alert is shown after the execution of the onDelete callback: my purpose is to show the alert before the deletion of a list element. Unfortunately the indexSet of the deleting element is know only by the onDelete callback.

2      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

I'm using CoreData. The template for Master / Detail with CoreData has this in the Event.swift

extension Collection where Element == Event, Index == Int {
    func delete(at indices: IndexSet, from managedObjectContext: NSManagedObjectContext) {
        indices.forEach { managedObjectContext.delete(self[$0]) }
        do {
            try managedObjectContext.save()
        } catch {
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }
}

The content view has this:

ForEach(events, id: \.self) { event in
  NavigationLink(
    destination: DetailView(event: event)
  ) {
    Text("\(event.timestamp!, formatter: dateFormatter)")
    }
  }.onDelete { indices in
    self.events.delete(at: indices, from: self.viewContext)
  }

So I would think you can make your own function for deleting (a class function in User), and only go there if the user agrees from the alert. And put the alert in .onDelete like you did.

2      

EDIT:

actually this should be even easier with

.alert(item: $deletedIndexSet) { indexSet in 
   return Alert(<YOUR ALERT>)  /// do some stuff with the indexSet here
}

this way you don't have to set the boolean and you also spare the force unwrap of the Optional (see here for reference: https://www.hackingwithswift.com/books/ios-swiftui/using-alert-and-sheet-with-optionals)

(btw. it's nice to see how I can improve my own answer due to learning more stuff 🥳)

original answer:


Hi @giurobrossi,

I've solved this issue (not having the indexSet after .onDelete is finished by having an

@State private var indexSetToDelete: IndexSet?

in my struct and setting this in the .onDelete:

@State private var showingAlert = false
@State private var deleteIndexSet: IndexSet?

<YOUR LIST>
.onDelete(perform: { indexSet in
    self.showingAlert = true
    self.deleteIndexSet = indexSet
})

.alert(isPresented: self.$showingAlert) {
    let indexSet = self.deleteIndexSet! /// you can force unwrap here because you only show the alert after .onDelete
    return Alert(<YOUR ALERT>)  /// do some stuff with the indexSet here
}

Hope that helps :)

(and I'm feeling great right now because this is the first time ever I shared my SwiftUI knowledge 🥰)

Greetings, Kevin

3      

Here is how I solved this. I wanted to write onConfirmedDelete anywhere I could call onDelete:

List {
    ForEach(model.seeds) { seed in
        Item(seed: seed)
    }
    .onMove { indices, newOffset in
        model.seeds.move(fromOffsets: indices, toOffset: newOffset)
    }
    .onConfirmedDelete(
        title: { indexSet in
            "Delete seed “\(model.seeds[indexSet.first!].name)”?"
        },
        message: "This cannot be undone.",
        action: { indexSet in
            model.seeds.remove(atOffsets: indexSet)
        }
    )
}

Here's how I implemented it:

extension DynamicViewContent {
    func onConfirmedDelete(title: @escaping (IndexSet) -> String, message: String? = nil, action: @escaping (IndexSet) -> Void) -> some View {
        DeleteConfirmation(source: self, title: title, message: message, action: action)
    }
}

struct DeleteConfirmation<Source>: View where Source: DynamicViewContent {
    let source: Source
    let title: (IndexSet) -> String
    let message: String?
    let action: (IndexSet) -> Void
    @State var indexSet: IndexSet = []
    @State var isPresented: Bool = false

    var body: some View {
        source
            .onDelete { indexSet in
                self.indexSet = indexSet
                isPresented = true
            }
            .alert(isPresented: $isPresented) {
                Alert(
                    title: Text(title(indexSet)),
                    message: message == nil ? nil : Text(message!),
                    primaryButton: .cancel(),
                    secondaryButton: .destructive(
                        Text("Delete"),
                        action: {
                            withAnimation {
                                action(indexSet)
                            }
                        }
                    )
                )
            }
    }
}

5      

You may want to think about UX. As .onDelete has two steps already.

  1. User taps EditButton
  2. Taps Delete Button

or

  1. User swipe left
  2. Taps Delete Button or Swipe further left

So add another check might not be the best. If you just had a Button that delete then setting a 2nd check in case of accidental touches is a good idea but .onDelete has this already.

2      

@NigelGee it's not fully right: the swipe gesture can be done in one gesture: swipe from right to far left. If you don't stop your finger in the middle, then there is no second step needed. If you say the user has two steps because he could stop in the middle, it's like saying that when pressing a button there is never need for confirmation because the user could, before releasing the button, swipe out of the button to cancel his tap... for data that are important, relying on such subtle and quick decision is too weak.

On my side I used a similar solution as @wolfmcnally, but i'm facing a wrong animation: I swipe > the row disapear (as if was deleted, while I did not already deleted it from the ForEach source) and reapears immediately > My alert opens > I confirm > the row is removed

looking at your comment, you don't seems to notice such behavior ?

2      

I'm correct in swipe to delete that the user has to make a long swipe to delete until red fills the whole of the row and if user moves back (before lifting finger) then it not deleted. A two step still

With regards to your data coming back after delete depending on the way your data is saved and without some code will not be able to tell but it sounds like that it not remove from the data set

2      

then you are agree that pushing a button is also a two steps action: press the button, and release it. and if the user wants to cancel he moves his finger out of the button before releasing ? For me a real two steps action can only be called like this if we force the user to raise the finger in between. Else the second action can be done too quickly (immediately), without having the time for revalidating what he is doing.

about the problem more related to this thread, here is the section of my code:

@State var showAlert = false
var itemToDelete: Item? // item is an NSManagedObject, from core data but I had the same issue with in-house model

@FetchRequest(...) // get all items
var items: FetchedResults<Item>

var body: some View {
    List {
        ForEach(items, id: \.id) { item in
            Text(item.name)
            .onDelete { indexes in
                itemToDelete = items[indexes.first!]
                showAlert = true
            }
        }
    }
    .alert(isPresented: $showAlert) {
        Alert(
            title: Text(itemToDelete.name!),
            message: "Are you sure you want to delete this ? This can't be undone...",
            primaryButton: .cancel(),
            secondaryButton: .destructive(
                Text("Delete"),
                action: {                   
                       DataStore.context.delete(itemToDelete)
                    try? DataStore.context.save()
                    showAlert = false
                }
            )
        )
}

as you see, I delete the item only in the alert code... but when swiping it, the item disapear and reapears immediately (it then diseapars correctly agin and definitively when confirming the deletion in alert)

2      

Hi @Ezeta31

That is the correct way the .onDelete works as the data is not delete when a new view(the alert) is presented so therefore it not deleted until it deleted in the alert. As I said before and if you look at apps that Apple make eg Mail or iMesage app etc you will not see an alert.

If you want to have this I would look at .onLongPressGesture or simlar and Paul article How to use gestures in SwiftUI show a number of way including a dragGesture

Hope that helps

Nigel

2      

Even Apple applications are requesting an explicit confirmation when you are about to delete definitively an important data. you can go for instance to vocal recordings app: if you delete the recording, it moves to a recently deleted section. If you go to this section and swipe to delete definitively the recording, there is a popup for confirming the deletion.

About my problem, the above example add an animation at the end of the swipe as if the row was deleted and immediately reapears. That's my problem. If you look at the apple vocal recording app, the row has not this glitch. So I assume there is a way to proceed to avoid it.

2      

Yes that is true but if you noticed that the recording is still there untill you tap the Delete Forever button in the ActionSheet. which is the behave that you have in your app. i thought you did not want this that way I suggested useing another way but if that what you want then no problem. You will also notice that if you just tap on the recording that you can Recover it from Recently Deleted.

2      

no that's not the behavior i have in my app... as said, in my app, the row disapears (quick animation where the row heigt reduces to 0, while red background fills all the line, even if don't drag my finger far at the left) and reapears (imediate animation) while the alert opens. when looking the apple voice recording app, this animation glitch does not occurs.

2      

Hi @Ezeta31

I was having a think about it and maybe do....

When .onDelete actually delete it from the data source but store it in a local variable then have the alert pop up with

"Are you sure you want to permanently delete this ? This can't be undone..."

and if the user Cancel then add it back (from local variable) to data source but if user Delete then do nothing

This should when swipe to delete disappear from the List and stay off the list unless the alert is canceled.

I know not like the "Voice Recording" app but if you want that I think you will have to build your own Swipe to Delete method.

Nigel

PS not sure if it will work as not tested it but it might a option to explore.

2      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.