More than 1 alert in SwiftUI [duplicate] - swift

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

Related

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.

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

How do I initialize an alert within a function using SwiftUI?

I'm trying to add an alert when gameOver() is called. "Result of 'Alert' initializer is unused". How do I initialize the alert I created?
func gameOver() {
round = 0
score = 0
self.changeTarget()
}
Solution Attempt:
func gameOver() {
round = 0
score = 0
self.changeTarget()
Alert(title: Text("Game Over"),
message: Text("Thanks for playing"),
dismissButton: Alert.Button.default( Text("Play Again")))
}
In SwiftUI framework you have several options for implementing Alert, for example:
func alert<Item>(item: Binding<Item?>, content: (Item) -> Alert) -> some View where Item : Identifiable
func alert<Item>(item: Binding<Item?>, content: (Item) -> Alert) -> some View where Item : Identifiable
Here is a simple example of using the first option:
struct GameOverAlert: View {
#State private var round = 0
#State private var score = 0
#State private var restartGame = false // variable for showing alert
var body: some View {
VStack {
Text("round: \(round)")
Text("score: \(score)")
HStack { // used this style just for brevity
Button(action: { self.score += 1 }) { Text("add score") }
Button(action: { self.gameOver() }) { Text("over game") }
}
Spacer() // only for presenting result
}
.alert(isPresented: $restartGame) {
Alert(title: Text("Your score is \(score)"), dismissButton: .default(Text("Play again")) {
self.playAgain()
})
}
}
// described logic here, but it should be in some ViewModel, etc
private func gameOver() {
restartGame = true
}
private func playAgain() {
score = 0
round = 0
}
}
with code above you'll achieve this:
This ended up working to display a game over alert.
.alert(isPresented: $alertIsVisible) { () -> Alert in
let roundedValue = sliderValueRounded()
if self.round == 5 {
return Alert(title: Text("Game Over"), message: Text("Your score was \(score)."), dismissButton: .default(Text("Play Again")) {
self.startOver()
})
} else {
return Alert(title: Text(alertTitle()), message: Text("The slider's value is \(roundedValue). \n" + "You scored \(pointsForCurrentRound()) points!"), dismissButton: .default(Text("Play Again")){
self.changeTarget()
self.round += 1
})}
}

ActionSheet/Sheet to Modal in SwiftUI

I am trying to create an actionSheet that opens a modal to save to a view. Currently, I can only get one of the buttons in the actionSheet to open to the modal. How can I rewrite the .sheet code to do this? Thanks.
#State var showActionSheet = false
#State var showAddActivityPopup = false
#State var showAddDayPopup = false
var body: some View {
Button(action: {
self.showActionSheet = true
}) {
Text("Show Action Sheet")
}
.actionSheet(isPresented: $showActionSheet, content: actionSheet)
.sheet(isPresented: $showAddDayPopup, onDismiss: {
print(self.showActionSheet)
}) {
AddDayPopup(message: "Add Day")
}
.sheet(isPresented: $showAddActivityPopup, onDismiss: {
print(self.showActionSheet)
}) {
AddActivityPopup(message: "Add Activity")
}
}
private func actionSheet() -> ActionSheet {
let button1 = ActionSheet.Button.default(Text("Add Day")) {
self.showActionSheet = false
self.showAddDayPopup = true
}
let button2 = ActionSheet.Button.default(Text("Add Activity")) {
self.showActionSheet = false
self.showAddActivityPopup = true
}
let actionSheet = ActionSheet(title: Text("What would you like to add?"),
message: nil,
buttons: [button1, button2, .cancel()])
return actionSheet
}
}

SwiftUI Change View with Button

I understand there is PresentationButton and NavigationButton in order to change views in the latest SwiftUI. However I want to do a simple operation like below. When user clicks on SignIn button if credentials are correct it will sign them in but also do a segue (in this case change the view). However I could not check if they are correct in PresentationButton and I could not change the view in a normal button.
Is there another way to do that?
#IBAction func signInClicked(_ sender: Any) {
if emailText.text != "" && passwordText.text != "" {
Auth.auth().signIn(withEmail: emailText.text!, password: passwordText.text!) { (userdata, error) in
if error != nil {
//error
} else {
performSegue(withIdentifier: "toFeedActivity", sender: nil)
}
}
} else {
//error
}
}
Here's one way.
struct AppContentView: View {
#State var signInSuccess = false
var body: some View {
return Group {
if signInSuccess {
AppHome()
}
else {
LoginFormView(signInSuccess: $signInSuccess)
}
}
}
}
struct LoginFormView : View {
#State private var userName: String = ""
#State private var password: String = ""
#State private var showError = false
#Binding var signInSuccess: Bool
var body: some View {
VStack {
HStack {
Text("User name")
TextField("type here", text: $userName)
}.padding()
HStack {
Text(" Password")
TextField("type here", text: $password)
.textContentType(.password)
}.padding()
Button(action: {
// Your auth logic
if(self.userName == self.password) {
self.signInSuccess = true
}
else {
self.showError = true
}
}) {
Text("Sign in")
}
if showError {
Text("Incorrect username/password").foregroundColor(Color.red)
}
}
}
}
struct AppHome: View {
var body: some View {
VStack {
Text("Hello freaky world!")
Text("You are signed in.")
}
}
}
I had the same need in one of my app and I've found a solution...
Basically you need to insert your main view in a NavigationView, then add an invisible NavigationLink in you view, create a #state var that controls when you want to push the view and change it's value on your login callback...
That's the code:
struct ContentView: View {
#State var showView = false
var body: some View {
NavigationView {
VStack {
Button(action: {
print("*** Login in progress... ***")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.showView = true
}
}) {
Text("Push me and go on")
}
//MARK: - NAVIGATION LINKS
NavigationLink(destination: PushedView(), isActive: $showView) {
EmptyView()
}
}
}
}
}
struct PushedView: View {
var body: some View {
Text("This is your pushed view...")
.font(.largeTitle)
.fontWeight(.heavy)
}
}
Try with state & .sheet
struct ContentView: View {
#State var showingDetail = false
var body: some View {
Button(action: {
self.showingDetail.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $showingDetail) {
DetailView()
}
}
}
You can use navigation link with tags so,
Here is the code:
first of all, declare tag var
#State var tag : Int? = nil
then create your button view:
Button("Log In", action: {
Auth.auth().signIn(withEmail: self.email, password: self.password, completion: { (user, error) in
if error == nil {
self.tag = 1
print("success")
}else{
print(error!.localizedDescription)
}
})
So when log in success tag will become 1 and when tag will become 1 your navigation link will get executed
Navigation Link code:
NavigationLink(destination: HomeView(), tag: 1, selection: $tag) {
EmptyView()
}.disabled(true)
if you are using Form use .disabled because here the empty view will be visible on form and you don't want your user to click on it and go to the homeView.