How to switch cancel and destructive alert buttons in SwiftUI? - ios

I created a simple alert and I would like to have the destructive button on the left and the 'cancel' button on the right, but I don't know how to manage it, any suggestions?
Code:
struct ContentView: View {
#State private var showAlert = false
var body: some View {
VStack {
Button("Present alert") {
showAlert.toggle()
}
}
.alert("Do you want to quit?",
isPresented: $showAlert,
actions: {
Button("No", role: .cancel) {
//
}
Button ("Yes", role: .destructive) {
//
}
}, message: {
Text("You will go back to main menu")
})
}
}

I've done a little research and the only solution I found is to use the deprecated struct Alert that still works but may not do so in the future, this is the code:
.alert(isPresented: $showAlert) {
Alert(
title: Text("Do you want to leave the game?"),
message: Text("You will go back to the main menu."),
primaryButton: .destructive(Text("Yes"), action: {
// code here
}),
secondaryButton: .default(Text("No")) //<-- use default here
)
}
Pay attention, you have to use .default() as a 'cancel' button, you cannot use .cancel()

Related

Navigation Bar back button disappears after showing an alert

I have a simple view:
struct AccountView: View {
#State private var showingLogoutAlert = false
var body: some View {
Button("Log out!") {
showingLogoutAlert = true
}
.alert(isPresented: $showingLogoutAlert, content: {
Alert(title: Text("Log out of Flow?"),
primaryButton: .cancel(),
secondaryButton: .destructive(Text("Log out"), action: {
//logOut()
}))
})
}
}
When you tap the button, it will show an alert. The problem is, after the alert dismisses, the back button also disappears! I have no idea why this is happening. See the gif below.
I've tried to reduce the problem down to the bare minimum code. Replicated it on a separate, new app. Still the same issue.
this is the working code I used in my test, on MacOS 13.2, Xcode 14.2, tested on real ios 16.3 devices (not Previews), and macCatalyst.
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink(destination: AccountView()) {
Text("AccountView")
}
}
}
}
struct AccountView: View {
#State private var showingLogoutAlert = false
var body: some View {
Button("Log out!") {
showingLogoutAlert = true
}
.alert(isPresented: $showingLogoutAlert, content: {
Alert(title: Text("Log out of Flow?"),
primaryButton: .cancel(),
secondaryButton: .destructive(Text("Log out"), action: {
//logOut()
}))
})
}
}

Why does my SwiftUI ActionSheet button/title display dark?

I have a common feature in my app where users can report posts but tapping a button and selecting "Report" from an action sheet.
For some reason my action-sheet title, .destructive AND .default buttons display darker than they should while the .cancel button displays as it should, see image below..
Below is my code and structure. I do not have any Extensions for ActionSheet that would be doing this and it occurs globally in my app, why could this be happening?
Thank you
Parent List View for each post
struct NewsFeed: View {
#EnvironmentObject var session : SessionStore
#StateObject var newsfeedVM = NewsFeedViewModel()
var body: some View {
NavigationView {
List {
ForEach(newsfeedVM.posts, id: \.id) { post in
PostCell(post: post)
}
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarTitle(Text(""), displayMode: .inline)
}
}
}
PostCell subview with ActionSheet
struct PostCell: View {
#StateObject var postVM = PostViewModel()
#State var post: Post
var body: some View {
VStack {
PostHeadline(postVM: postVM, post: $post)
PostMedia(postVM: postVM, post: $post)
CommentCell(postVM: postVM, post: $post)
}
.actionSheet(isPresented: $postVM.showActionSheet) {
ActionSheet(title: Text("Report a post"), buttons: [
.destructive(Text("Report")) {
self.reportPost(post: post)
},
.cancel() {
postVM.selectedPost = ""
}
])
}
}
func reportPost(post: Post) {/// }
}

Multiple Alerts in one view can not be called SwiftUI

The method of calling an alert in swiftUI is supposed to be simpler than ever, a simple example of this would be
struct ContentView: View {
#State private var showingAlert = false
var body: some View {
Button(action: {
self.showingAlert = true
}) {
Text("Show Alert")
}
.alert(isPresented: $showingAlert) {
Alert(title: Text("Important message"), message: Text("Wear sunscreen"), dismissButton: .default(Text("Got it!")))
}
}
}
However, even though I am calling an alert and can verify the method is being called, my alert does not display.
In my code I have the following Struct
private struct resetButton: View {
#State var isResetting = false
var body: some View {
Button("Reset"){
self.isResetting = true
}.alert(isPresented: $isResetting) {
print("We get here")
return
Alert(title: Text("Are you sure?"), message: Text("This will erase your Access Key."), primaryButton: .destructive(Text("Reset")) {
clearToken()
clearAccessKey()
clearUsername()
}, secondaryButton: .cancel(Text("Cancel")))
}
}
}
Which is called in the main view simply by resetButton().
However, when I push this button, despite isResetting equalling true, the alert does not display. I have verified that my .alert method is being called as the console prints
we get here
the first time the button is pushed.
However, the Alert is never displayed.
An alert is displayed correctly elsewhere in this same view with
.alert(isPresented: $failedLogin) {
self.password = ""
return
Alert(title: Text("Invalid Login"), message: Text("Please verify your credentials and try again"), dismissButton: .default(Text("Ok")))
}
When a #State variable named failedLogin is set true, however, the aforementioned alert is never presented.
My first impression is that this may be a bug in Swift, if it is I'll report it to apple. However, maybe something I'm doing isn't working because of an error on my part.
Edit:
As clarified in the answer below, the problem seems to be relating to the fact that my resetButton() was trying to throw up an alert in a view that already contains an alert. Apparently you can't have multiple alerts in one view.
You can show alerts dynamically by using .alert(item:) instead .alert(isPresented:):
struct AlertItem: Identifiable {
var id = UUID()
var title: Text
var message: Text?
var dismissButton: Alert.Button?
}
struct ContentView: View {
#State private var alertItem: AlertItem?
var body: some View {
VStack {
Button("First Alert") {
self.alertItem = AlertItem(title: Text("First Alert"), message: Text("Message"))
}
Button("Second Alert") {
self.alertItem = AlertItem(title: Text("Second Alert"), message: nil, dismissButton: .cancel(Text("Some Cancel")))
}
Button("Third Alert") {
self.alertItem = AlertItem(title: Text("Third Alert"))
}
}
.alert(item: $alertItem) { alertItem in
Alert(title: alertItem.title, message: alertItem.message, dismissButton: alertItem.dismissButton)
}
}
}
So i'm still not 100% sure why this was unable to post an alert this way? But I was able to fix this issue simply by placing the alert in the main view.
I changed the #State variable isResetting to a binding bool and paced a new state variable in the main class. I then just copied over the .alert and this seems to solve the issue.
The struct now looks like this
private struct resetButton: View {
#Binding var isResetting: Bool
var body: some View {
Button("Reset"){
self.isResetting = true
}
}
}
and the alert is the same but is now in the class calling resetButton().
Edit:
So it appears that SwiftUI won't let you call multiple alerts from the same view, however, if you want multiple alerts in the same view then there is a way around this.
You can call an alert from anywhere inside of a view so the following solution works.
private struct exampleAlert: View {
#State var alert1 = false
#State var alert2 = false
var body: some View {
Vstack{
Button("alert 1"){
self.alert1 = true
}.alert(isPresented: $alert1) {
Alert(title: Text("Important message"), message: Text("This alert can show"), dismissButton: .default(Text("Got it!")))
}
Button("alert 2"){
self.alert2 = true
}.alert(isPresented: $alert2) {
Alert(title: Text("Important message"), message: Text("This alert can also show"), dismissButton: .default(Text("Got it!")))
}
}
}
}
The catch is here that if either of these alerts were to be placed on the Vstack, one will not function. If they are placed on separate views though then both can be called as expected.
Perhaps this is something that will be rectified in a future update? In the meantime though, here is a solution for working around this problem.
In my case I solved it by adding an empty Text in front of the alert.
private struct exampleAlert: View {
#State var alert1 = false
#State var alert2 = false
var body: some View {
Vstack{
Button("alert 1"){
self.alert1 = true
}
Button("alert 2"){
self.alert2 = true
}
}
Vstack{
Text("")
.alert(isPresented: $alert1) {
Alert(title: Text("Important message"), message: Text("This alert can show"), dismissButton: .default(Text("Got it!")))
}
Text("")
.alert(isPresented: $alert2) {
Alert(title: Text("Important message"), message: Text("This alert can also show"), dismissButton: .default(Text("Got it!")))
}
}
}
}

SwiftUI: How to present new views on clicking ActionSheet button?

I have an action sheet consist of three actions
Action_1
Action_2
Cancel
If I tap on 'Action_1', My app should present ActionView_1().
If I tap on 'Action_2', My app should present ActionView_2().
I got this for presenting a view
.sheet(isPresented: $isAddSecretVisible){ActionView_1()}.
But this for presenting a view with a Button click.
I need the same action if tap on the Actionsheet button.
Answer needs in SwiftUI
Thanks in advance.
You should define 2 State, each for each a sheet:
#State var isMainActionPresented = false
#State var isActionViewPresented = false
And a State to determine witch actionSheet to present. So you can have an enum for that like:
enum ActionViewMode {
case first
case second
}
And a helper extension on that:
extension ActionViewMode {
var view: some View {
switch self {
case .first: return ActionView1()
case .second: return ActionView2()
}
}
}
Then on click of any Button or ActionSheet.Button, toggle the desired state. Look at the complete ContentView code below:
#State var actionViewMode = ActionViewMode.first
#State var isMainActionPresented = false
#State var isActionViewPresented = false
var body: some View {
Button(action: {
self.isMainActionPresented = true
}) {
Text("ActionSheet")
}
.actionSheet(isPresented: $isMainActionPresented) {
ActionSheet(
title: Text("Title"),
message: Text("Message"),
buttons: [
.default(
Text("Action_1"),
action: {
self.actionViewMode = .first
self.isActionViewPresented = true
}),
.default(
Text("Action_2"),
action: {
self.actionViewMode = .second
self.isActionViewPresented = true
}),
.cancel()
])
}
.sheet(isPresented: $isActionViewPresented) {
self.actionViewMode.view
}
}
SwiftUI will handle the rest.
Note that you can't chain multiple sheets one after another, because each one overrides previous somehow.
Reference link : SwiftUI
ActionSheet(title: Text("iOSDevCenters"), message: Text("SubTitle"), buttons: [
.default(Text("Action_1"), action: {
print("Action_1")
ActionView_1()
}),
.destructive(Text("Cancel"))
])
})

SwiftUI: How to execute closure when Alert is dismissed?

I've been trying out swiftUI and looked at this Ray Wenderlich tutorial... I noticed they didn't re-implement the "nextRound" functionality... so I tried to do it myself. Ran into a problem (which maybe they did, also):
The basic question is more general:
Using swiftUI, how do you trigger a function when an Alert is dismissed -- when the user clicks "OK." ?
I've tried using the dismissButton argument of the Alert constructor...
(and also the .onDisappear method of View but I can't figure out how to apply it to the Alert view.)
Code:
import SwiftUI
struct ContentView: View {
#State var shouldShowAlert: Bool = false
// this never gets called
func onAlertDismissed() {
print("you will not see this in the console")
}
// this doesn't seem to work
var dismissButton: some View {
Button(action: {
self.onAlertDismissed()
}) {
// Bilbo Baggins does not appear -- "OK" still shows
Text("BILBO BAGGINS")
}
}
var body: some View {
VStack {
Spacer()
Button(action: {
self.shouldShowAlert = true
}) {
Text("show the alert!")
}
Spacer()
}.alert(isPresented: $shouldShowAlert, content: {
// what to add here?
Alert(title: Text("Alert:"), message: Text("press OK to execute onAlertDismissed()..."))
// what I have tried and doesn't work:
/*
Alert(title: Text("Alert:"), message: Text("press OK to execute onAlertDismissed()..."), dismissButton: self.dismissButton as? Alert.Button)
*/
})
}
}
The button is constructed a little differently. You basically have to use a static factory method from Alert.Button to construct them and pass those in.
Alert(title: Text("Alert:"),
message: Text("press OK to execute default action..."),
dismissButton: Alert.Button.default(
Text("Press ok here"), action: { print("Hello world!") }
)
)
Alert(title: Text("Alert!"), message: Text("Message"),
primaryButton: Alert.Button.default(Text("Yes"), action: {
print("Yes")
}),
secondaryButton: Alert.Button.cancel(Text("No"), action: {
print("No")
})
)
It's possible to create alerts like this:
import SwiftUI
struct ContentView: View {
#State var showingAlert = false
var body: some View {
VStack {
HStack {
Button(action: {
self.showingAlert = true
})
{
Text("Save")
.font(.headline)
}
.alert(isPresented: $showingAlert, content: {
return Alert(
title: Text("Save Product"),
message: Text("Are you sure you want to save the changes made?"),
primaryButton: .default(Text("Yes"), action: {
//insert an action here
}),
secondaryButton: .destructive(Text("No")))
})
}
}
}
}
By looking at your code, it appears you don’t include a button in the alert propert, so your alert is not executing any action, in swiftui the alert signature is
init(title: Text, message: Text? = nil, primaryButton: Alert.Button, secondaryButton: Alert.Button)
Implement the signature properly is the first step

Resources