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

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!"))
}
}
}

Related

Swift message alert exit button doesn't work

I am creating an application in which a user, based on the permissions he has, can access the various views.
I use this method to constantly check user permissions:
func checkPermission() {
let docRef = self.DatabaseFirestore.collection("Admins").document(phoneNumber)
docRef.getDocument{(document, error) in
guard error == nil else {
return
}
if let document = document, document.exists {
self.controlloAdmin = true
guard let data = document.data() else {
print("Document data was empty.")
return
}
self.permission = data["Permessi"] as? [Bool] ?? []
} else {
self.controlloAdmin = false
self.isRegistred = false
self.access = false
}
}
}
I don't know if it is the most correct function I could use, but it is one of the few that I have found that works.
This is my view:
struct AdministratorPage: View {
#StateObject var administratorManager = AdministratorManager()
// User variables.
#AppStorage("phoneNumber") var phoneNumber: String = "" // User number.
#AppStorage("Access") var access: Bool = false
var body: some View {
administratorManager.checkPermission()
return NavigationView {
HStack {
VStack {
Text("Home")
Text(phoneNumber)
// Button to log out.
Button("Logout", action: {
self.access = false
})
Button("Alert", action: {
administratorManager.message = "Error title!"
administratorManager.message = "Error message!"
administratorManager.isMessage = true
}).alert(isPresented: $administratorManager.isMessage) {
Alert(title: Text(administratorManager.title), message: Text(administratorManager.message),
dismissButton: .default(Text("Ho capito!")))
}
}
}
}
}
}
When I call the "administratorManager.checkPermission()" function and press the "Alert" button the message is displayed, but even if the button is pressed the alert does not disappear. If I don't call this function, everything works.
How can I solve? Can the alert go against firebase? Is there a more suitable method to read only one data?
photo of the screen when it got locked
I ran your code and I saw the behavior you described.
The reason is the function call directly in the body.
If you want to call a function when when you open a view, use the .onAppear function for that specific view. In your case
.onAppear {
administratorManager.checkPermission()
}
The following (worked for me with you code):
struct AdministratorPage: View {
#StateObject var administratorManager = AdministratorManager()
// User variables.
#AppStorage("phoneNumber") var phoneNumber: String = "" // User number.
#AppStorage("Access") var access: Bool = false
var body: some View {
return NavigationView {
HStack {
VStack {
Text("Home")
Text(phoneNumber)
// Button to log out.
Button("Logout", action: {
self.access = false
})
Button("Alert", action: {
administratorManager.message = "Error title!"
administratorManager.message = "Error message!"
administratorManager.isMessage = true
}).alert(isPresented: $administratorManager.isMessage) {
Alert(title: Text(administratorManager.title), message: Text(administratorManager.message),
dismissButton: .default(Text("Ho capito!")))
}
}
}
}
.onAppear {
administratorManager.checkPermission()
}
}
}
UPDATE: add Snapshot listener instead of polling
Your initial approach was doing a kind of polling, it called the function constantly. Please keep in mind, when you do a Firebase request, you will be billed for the documents you get back. If you do the polling, you get the same document multiple times and will be billed for it.
With my above mentioned example in this answer, you just call the function once.
If you now want to get the live updated from Firestore, you can add a snapshot listener. The approach would be:
func checkPermission() {
let docRef = db.collection("Admins").document(phoneNumber).addSnapshotListener() { documentSnapshot, error in //erca nella collezione se c'è il numero.
guard error == nil else {
print("ERROR.")
return
}
if let document = documentSnapshot {
self.controlloAdmin = true
guard let data = document.data() else {
print("Document data was empty.")
return
}
self.permission = data["Permessi"] as? [Bool] ?? []
} else {
self.controlloAdmin = false
self.isRegistred = false
self.access = false
}
}
}
Whenever a value changed on that document in Friestore, it'll be changed on your device as well.
Best, Sebastian

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.

More than 1 alert in SwiftUI [duplicate]

I want to immediately present the second alert view after click the dismiss button of the first alert view.
Button(action: {
self.alertIsVisible = true
}) {
Text("Hit Me!")
}
.alert(isPresented: $alertIsVisible) { () -> Alert in
return Alert(title: Text("\(title)"), message: Text("\n"), dismissButton:.default(Text("Next Round"), action: {
if self.score == 100 {
self.bonusAlertIsVisible = true
}
.alert(isPresented: $bonusAlertIsVisible) {
Alert(title: Text("Bonus"), message: Text("You've earned 100 points bonus!!"), dismissButton: .default(Text("Close")))}
})
)
However, it gives me an error of 'Alert.Button' is not convertible to 'Alert.Button?'
If I put this segment out of the scope of dismissButton, it will override the previous .alert.
So how can i do it, I just want to pop up the second alert after clicking the dismiss button of the first alert.
Thanks.
It appears (tested with Xcode 11.2):
While not documented, but it is not allowed to add more than one
.alert modifier in one view builder sequence - works only latest
It is not allowed to add .alert modifier to EmptyView, it does not work
at all
I've found alternate solution to proposed by #Rohit. In some situations, many alerts, this might result in simpler code.
struct TestTwoAlerts: View {
#State var alertIsVisible = false
#State var bonusAlertIsVisible = false
var score = 100
var title = "First alert"
var body: some View {
VStack {
Button(action: {
self.alertIsVisible = true
}) {
Text("Hit Me!")
}
.alert(isPresented: $alertIsVisible) {
Alert(title: Text("\(title)"), message: Text("\n"), dismissButton:.default(Text("Next Round"), action: {
if self.score == 100 {
DispatchQueue.main.async { // !! This part important !!
self.bonusAlertIsVisible = true
}
}
}))
}
Text("")
.alert(isPresented: $bonusAlertIsVisible) {
Alert(title: Text("Bonus"), message: Text("You've earned 100 points bonus!!"), dismissButton: .default(Text("Close")))
}
}
}
}
Please try below code.
Consecutively present two alert views using SwiftUI
struct ContentView: View {
#State var showAlert: Bool = false
#State var alertIsVisible: Bool = false
#State var bonusAlertIsVisible: Bool = false
var body: some View {
NavigationView {
Button(action: {
self.displayAlert()
}) {
Text("Hit Me!")
}
.alert(isPresented: $showAlert) { () -> Alert in
if alertIsVisible {
return Alert(title: Text("First alert"), message: Text("\n"), dismissButton:.default(Text("Next Round"), action: {
DispatchQueue.main.async {
self.displayAlert()
}
})
)
}
else {
return Alert(title: Text("Bonus"), message: Text("You've earned 100 points bonus!!"), dismissButton:.default(Text("Close"), action: {
self.showAlert = false
self.bonusAlertIsVisible = false
self.alertIsVisible = false
})
)
}
}
.navigationBarTitle(Text("Alert"))
}
}
func displayAlert() {
self.showAlert = true
if self.alertIsVisible == false {
self.alertIsVisible = true
self.bonusAlertIsVisible = false
}
else {
self.alertIsVisible = false
self.bonusAlertIsVisible = true
}
}
}

config 2 alerts messages in button swiftUI

I going learn swift and swiftUI.
I make application for organize notes by category. you can find my project in my GitHub if you need. https://github.com/yoan8306/List-Notes
I have problem. I think it's simple. I would like make 2 alerts messages. The first it's when save is success and the second is when they are problem like one field is empty or category is empty.
private func checkNoteIsOk() -> Bool{
if !noteTitleField.isEmpty && !noteField.isEmpty && categorySelected != nil {
return true
} else {
return false
}
}
.
Button(action: {
guard checkNoteIsOk() else {
presentAlert = true
return
}
coreDM.saveNote(noteData: noteField, noteTitle: noteTitleField,
noteDate: Date(), noteCategory: categorySelected!)
emptyField()
saveSuccess = true
},
label: {
Text("Save")
}
)
}
//end Vstak
.navigationTitle("Create new note")
.alert(isPresented: $presentAlert) {
Alert(title: Text("Error !"), message: Text("Not saved"),
dismissButton: .default(Text("OK"))) }
.alert(isPresented: $saveSuccess) {
Alert(title: Text("Success !"), message: Text("Insert with success !"),
dismissButton: .default(Text("OK"))) }
I think it's because they are two alerts messages. And only the last message alert can display. Thank you for your answer and your help.
For multiple alerts in a single view, you can use an enum.
First, you need to create an enum like this and define all the alert message
enum AlertType: Identifiable {
var id: UUID {
return UUID()
}
case success
case error
var title: String {
switch self {
case .success:
return "Success !"
case .error:
return "Error !"
}
}
var message: String {
switch self {
case .success:
return "Insert with success !"
case .error:
return "This category already exist !!"
}
}
}
now create one state var in the view.
struct NewCategoryView: View {
#State private var alertType: AlertType?
// Other code
}
and add the alert at the end
//end Vstak
.navigationTitle("New Category")
.onAppear(perform: { updateCategoryList()} )
.alert(item: self.$alertType, content: { (type) -> Alert in
Alert(title: Text(type.title), message: Text(type.message),
dismissButton: .default(Text("OK")))
})
now show the alert by assigning the value. Like this
if condition_true {
alertType = AlertType.success //<-- Here
} else {
alertType = AlertType.error //<-- Here
}
While [Raja]'s answer is working. I don't think it is ideal because
it generates random UUID's which Apple discourages when it's not needed.
it does requires multiple switch statements where only one is needed.
A more simple solution might be to define the enum like this
enum ResultAlert: Int8, Identifiable {
case success, error
var id: some Hashable { rawValue }
var content: Alert {
switch self {
case .success: return Alert(title: Text("Success!"))
case .error: return Alert(title: Text("Oy, error..."))
}
}
}
Then the rest is the same as Raja's answer:
Add it as a #State variable to your view
#State var resultAlert: ResultAlert?
Activate it using resultAlert = .success or resultAlert = .error. Deactivate it using resultAlert = .none
And present it like this:
.alert(item: $resultAlert, content: \.content)

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

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())
}