SwiftUI - Add delete action (with index) to Alert button - swift

I have a DataManager in which I have the following delete func:
func deleteValue(index: Int) {
storage.remove(at: index)
save()
}
And then in another view I have all my values into a Form. I wouldn't like to use the .onDelete because I would like to have an Alert that lets the user decide if he/she really wants to delete the value, but by doing this I must insert the index. How do I do this? Here's the code:
#State var showAlertDelete = false
var dm : DataManager
var deleteButton : some View {
Button(action: {
showAlertDelete = true
}) { Text("Delete").foregroundColor(Color.red)
}.alert(isPresented: $showAlertDelete, content: {
deleteValueAlert
})
}
func deleteValue(at offset: IndexSet) {
guard let newIndex = Array(offset).first else { return }
dm.deleteValue(index: newIndex)
}
var deleteValueAlert : Alert {
Alert(title: Text("Are you sure you want to delete this?"), primaryButton: Alert.Button.default(Text("Yes")){ deleteValue //Here it says that I must add the init with the index }, secondaryButton: Alert.Button.cancel(Text("No")))
}
How can I solve this? Thanks to everyone!

You can use .onDelete by also presenting an Alert when triggered, simply add:
#State private var showAlert = false
#State private var indexSetToDelete: IndexSet?
And then in your .onDelete method:
.onDelete { (indexSet) in
self.showAlert = true
self.indexSetToDelete = indexSet
}
Last step would be to add the Alert in your view body where you can call your delete method:
.alert(isPresented: $showAlert) {
Alert(title: Text("Confirm Deletion"),
message: Text("Are you sure you want to delete xxx?"),
primaryButton: .destructive(Text("Delete")) {
self.deleteValue(indexSet: self.indexSetToDelete!) //call delete method
},
secondaryButton: .cancel())
}

Related

Why alert dialog delete the wrong item in SwiftUI?

I have a problem in my SwiftUI project, and I am trying to use alert dialog to delete an item in a list, but when use the .alert, wrong item gets deleted. I am still do not know what I missed?
Here is my code:
struct CustomView: View {
#State private var selectedUsers: CustomModel?
#State var users: [CustomModel]
#State private var selectDelete = false
var body: some View {
ScrollView(.vertical, showsIndicators: false, content: {
VStack(content: {
ForEach(users){ user in
CustomRowView(user: user)
.contextMenu {
Button(action: {
self.delete(item: data)
}) {
Text("remove")
}
}
.onTapGesture {
selectedUsers = user
}
.alert(isPresented: $selectDelete) {
Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.delete(item: user)
},
secondaryButton: .cancel()
)
}
.onDelete { (indexSet) in
self.users.remove(atOffsets: indexSet)
}
}
})
})
}
private func delete(item user: CustomModel) {
if let index = users.firstIndex(where: { $0.id == user.id }) {
users.remove(at: index)
}
}
}
model:
struct CustomModel: Identifiable{
var id = UUID().uuidString
var name: String
}
var users = [
CustomModel(name: "david"),
CustomModel(name: "marry"),
CustomModel(name: "henry"),
CustomModel(name: "nadi"), ]
There are 2 problems in your code that are creating the unexpected behaviour:
When you iterate through the user with ForEach, you are attaching one .alert() modifier to each single instance. This means, when selectDelete is set to true, all of the instances try to show an alert, but only one will. Which one? Who knows, but that instance will be deleted.
You have a nice selectedUsers variable that changes when you tap on it. But you are deleting the user, not selectedUser.
How you can fix your code to work with .alert() in 3 steps:
if you don't need to perform any task with the tap gesture, just delete it and change the selectedUser in your context menu:
.contextMenu {
Button(action: {
selectedUsers = user // Change here then delete
self.delete(item: data)
}) {
Text("remove") }}
// Forget about it...
//.onTapGesture {
// selectedUsers = user
//}
attach your alert to the top-most level of your view (at the bottom):
ScrollView {
...
}
.alert(isPresented: $selectDelete) {
...
}
delete selectedUser, not user:
self.delete(item: selectedUser)

Why I am not able to use multiple `.alert` dialog in my SwiftUI project?

I want to delete list items and when I delete list items, it will show confirmation dialog like .alert dialog. I have code below and if I want to remove list item .alert dialog is work, but if I try to remove all list items, .alert dialog not work, and I am not able to remove all items, I do not know where I missed? I guess most probably it is due to the I have two .alert dialog and they are conflicted, any idea?
struct CustomView: View {
#State private var selectedUsers: CustomModel?
#State var users: [CustomModel]
#State private var selectDelete = false
#State private var selectAllDelete = false
var body: some View {
ScrollView(.vertical, showsIndicators: false, content: {
VStack(content: {
ForEach(users){ user in
CustomRowView(user: user)
.contextMenu {
Button(action: {
selectDelete = true
}) {
Text("remove")
}
Button(action: {
selectAllDelete = true
}) {
Text("remove all")
}
}
.alert(isPresented: $selectDelete) {
Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.delete(item: data)
},
secondaryButton: .cancel()
)
}
.alert(isPresented: $selectAllDelete) {
Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.datas.removeAll()
},
secondaryButton: .cancel()
)
}
.onDelete { (indexSet) in
self.users.remove(atOffsets: indexSet)
}
}
})
})
}
private func delete(item user: CustomModel) {
if let index = users.firstIndex(where: { $0.id == user.id }) {
users.remove(at: index)
}
}
}
model:
struct CustomModel: Identifiable{
var id = UUID().uuidString
var name: String
}
var users = [
CustomModel(name: "david"),
CustomModel(name: "marry"),
CustomModel(name: "henry"),
CustomModel(name: "nadi"), ]
You can create an alert type and handle it using switch statement.
enum AlertType {
case selectDelete
case selectAllDelete
}
private var alertType: AlertType?
#State private var isAlertPresented = false
...
Button(action: {
alertType = .selectDelete
isAlertPresented = true
}) {
Text("remove all")
}
...
.alert(isPresented: $isAlertPresented) {
presentAlert()
}
...
func presentAlert() -> Alert {
switch alertType {
case .selectDelete:
return Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.delete(item: data)
},
secondaryButton: .cancel())
case .selectAllDelete:
return Alert(title: Text("title"),
message: Text("message"),
primaryButton: .destructive(Text("Delete")) {
self.datas.removeAll()
},
secondaryButton: .cancel())
default:
return Alert(title: Text(""))
}
}
If you apply the modifier to each Button it'll work. Also, you might find confirmationDialog more suitable for this task.
Move your Buttons into custom Views will help too because body has a 10 View limit.

How we can use alert menu before delete list items in SwiftUI?

I have list items in SwiftUI, and when I delete list items I want to delete after alert menu, like
"do want to delete your list items, ""yes" or "no"
is it possible?
struct MyView: View {
#State private var selectedUsers: MyModel?
var body: some View {
ScrollView(.vertical, showsIndicators: false, content: {
VStack(content: {
ForEach(datas){ data in
MyRowView(data: data)
.contextMenu {
Button(action: {
self.delete(item: data)
}) {
Text("delete")
}
}
.onTapGesture {
selectedUsers = data
}
} .onDelete { (indexSet) in
self.datas.remove(atOffsets: indexSet)
}})
})}
private func delete(item data: MyModel) {
if let index = datas.firstIndex(where: { $0.id == data.id }) {
datas.remove(at: index)
}
}}
In the delete action you set a #State bool to true, this triggers e.g. a ConfirmationDialog – and only after confirming there, you really delete:
} .onDelete { (indexSet) in
confirmDelete = true // a #State var Bool
}})
.confirmationDialog("Do you really want to delete?", isPresented: $confirmDelete) {
Button("Delete", role: .destructive) {
selectedUsers.remove(atOffsets: indexSet)
}
Button("Cancel", role: .cancel) { }
}

How to get current IndexSet in a List SwiftUI? [duplicate]

I'm trying to delete a list item with context menu.
The data is fetched from Core Data.
.onDelete works as expected with my deleteExercise func without further do.
But when calling the deleteExercise within the context menu button, it asks for the IndexSet which I honestly have no idea where to get from.
I am also wondering why I don't need to specify the IndexSet when using .onDelete
struct ExercisesView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
animation: .default)
private var exercises: FetchedResults<Exercise>
var body: some View {
NavigationView {
List {
ForEach(exercises) { e in
VStack {
NavigationLink {
ExerciseDetailView(exercise: e)
} label: {
Text(e.name ?? "")
}
}
.contextMenu { Button(role: .destructive, action: { deleteExercise(offsets: /* Index Set */) }) {
Label("Delete Exercise", systemImage: "trash")
} }
}
.onDelete(perform: deleteExercise)
}
}
}
private func deleteExercise(offsets: IndexSet) {
withAnimation {
for index in offsets {
let exercise = exercises[index]
viewContext.delete(exercise)
}
viewContext.save()
}
}
}
Instead of trying to derive an IndexSet from ForEach, which doesn't immediately expose one for you, you could create a separate delete method:
.contextMenu { Button(role: .destructive, action: {
deleteExercise(exercise)
}) {
Label("Delete Exercise", systemImage: "trash")
} }
func deleteExercise(_ exercise: Exercise) { //I'm making an assumption that your model is called Exercise
withAnimation {
viewContext.delete(exercise)
viewContext.save()
}
}
In regards to your last question:
I am also wondering why I don't need to specify the IndexSet when using .onDelete
You don't need to specify it because it's sent as a parameter by onDelete -- that's what your deleteExercise(offsets:) is receiving from the onDelete modifier.

SwiftUI – Not clear how to handle the alert with asynchronous methods

I have to present an Alert on a view if the user taps on it.
My alert depends on several situations:
Item is purchased. Show the item.
Item have not be purchased. Show an alert telling the user the item have to be purchased. This alert must show two buttons, OK to purchase, Cancel to dismiss.
User taps to purchase the item.
Purchase is successful, show the item.
Purchase fails, show error.
This is how I did it.
class AlertDialog {
enum SelectedType {
case none
case purchase
case mustBePurchased
case purchaseError
}
var selectedType:SelectedType = .none
}
struct FilteredListItem: View {
#State var showAlert: Bool = false
private var alertDialog:AlertDialog?
var body: some View {
Text(item.termLowerCase)
.font(fontItems)
.foregroundColor(.white)
.onTapGesture {
DispatchQueue.main.async {
appStoreWrapper.verifyPurchase(productID: item.package!)
{ // run if purchased
purchased = true
} runIfNotPurchased: {
purchased = false
alertDialog!.selectedType = .mustBePurchased
showAlert = true
}
}
}
.alert(isPresented: $showAlert) {
if alertDialog!.selectedType == .purchase {
appStoreWrapper.purchase(productID: item.package!) {
// run if purchased
purchased = true
} runIfPurchaseFailed: { (error) in
alertDialog!.selectedType = .purchaseError
appStoreWrapper.purchaseError = error
showAlert = true
}
} else if alertDialog!.selectedType == .purchaseError {
let primaryButton = Alert.Button.default(Text("OK")) {
showAlert = false
}
return Alert(title: Text(appStoreWrapper.makeString("ERROR")),
message: Text(appStoreWrapper.purchaseError),
dismissButton: primaryButton)
}
let dismissButton = Alert.Button.default(Text(appStoreWrapper.makeString("CANCEL"))) {
showAlert = false
}
let primaryButton = Alert.Button.default(Text("OK")) {
appStoreWrapper.purchase(productID: item.package!) {
// run if purchased
purchased = true
} runIfPurchaseFailed: { (error) in
appStoreWrapper.purchaseError = error
alertDialog!.selectedType = .purchaseError
showAlert = true
print(erro)
}
}
return Alert(title: Text(appStoreWrapper.makeString("ERROR")),
message: Text(appStoreWrapper.purchaseError),
primaryButton: primaryButton,
secondaryButton: dismissButton)
}
This is my problem: the modifier .alert(isPresented: $showAlert) expects an Alert() to be returned, right? But I have these asynchronous methods
appStoreWrapper.verifyPurchase(productID: item.package!)
{ // run if purchased },
runIfNotPurchased: { }
that cannot return anything to the alert modifier. How do I do that? Is what I am doing right?
There's a lot going on in your code and you didn't post the code for appStoreWrapper, but here's some code that should be able to point you in the right direction.
FYI:
You can use a Button with an Action instead of using Text with .onTapGesture
The code within .Alert should only function to get an Alert. You shouldn't be doing other actions within the .Alert closure.
struct FilteredListItem: View {
#State var showAlert: Bool = false
private var alertDialog: AlertDialog?
var body: some View {
Button(action: {
verifyItem()
}, label: {
Text("ITEM NAME")
.foregroundColor(.white)
})
.accentColor(.primary)
.alert(isPresented: $showAlert, content: {
getAlert()
})
}
func verifyItem() {
// FUNCTION TO VERIFY ITEM HERE
var success = true //appStoreWrapper.verifyPurchase...
if success {
// Handle success
} else {
alertDialog?.selectedType = .mustBePurchased
showAlert.toggle()
}
}
func purchaseItem() {
// FUNCTION TO PURCHASE ITEM HERE
var success = true //appStoreWrapper.purchase...
if success {
// Handle success
} else {
alertDialog?.selectedType = .purchaseError
showAlert.toggle()
}
}
func getAlert() -> Alert {
guard let dialog = alertDialog else {
return Alert(title: Text("Error getting alert dialog."))
}
switch dialog.selectedType {
case .purchaseError:
return Alert(
title: Text("Error purchasing item."),
message: nil,
dismissButton: .default(Text("OK")))
case .mustBePurchased:
return Alert(
title: Text("Items have to be purchased."),
message: nil,
primaryButton: .default(Text("Purchase"), action: {
purchaseItem()
}),
secondaryButton: .cancel())
case .none, .purchase:
return Alert(title: Text("Purchased!"))
}
}
}