SwiftUI Multiple alerts for same button [duplicate] - swiftui

This question already has answers here:
how to show different alerts based on a condition after clicking a button in swiftui
(3 answers)
Closed 2 years ago.
The code below only shows the false alert. Is there a way to make the alert match the IF condition?
#State var showTrueAlert = false
#State var showFalseAlert = false
var body: some View {
Button(action: {
let isTrue = Bool.random()
if isTrue {
self.showTrueAlert = true
print("True Alert")
} else {
self.showFalseAlert = true
print("False Alert")
}
}) {
Text("Random Alert")
.font(.largeTitle)
}
.alert(isPresented: $showTrueAlert) {
Alert(title: Text("True"))
}
.alert(isPresented: $showFalseAlert) {
Alert(title: Text("False"))
}
}

You can apply .alert only once to a View. Create a State which only handles the current State of the Alert and then two variables which decide if false or true was pressed. (might store that in one variable only aswell)
struct ContentView6: View {
#State var showAlert : Bool = false
#State var showTrueAlert = false
#State var showFalseAlert = false
var body: some View {
Button(action: {
let isTrue = Bool.random()
if isTrue
{
self.showTrueAlert = true
self.showAlert = true
print("True Alert")
} else {
self.showFalseAlert = true
self.showAlert = true
print("False Alert")
}
}) {
Text("Random Alert")
.font(.largeTitle)
}
.alert(isPresented: Binding<Bool>(
get: {
self.showAlert
},
set: {
self.showAlert = $0
self.showTrueAlert = false
self.showFalseAlert = false
})) {
if (showTrueAlert)
{
return Alert(title: Text("True"))
}
else
{
return Alert(title: Text("False"))
}
}
}
}

Related

SwiftUI - Why doesn't my .alert window change my state variable?

The incomprehensible thing is that if I set breakpoints when debugging, I get my desired result. But now to the problem:
I have a view, which on appearance should check if errors have appeared in previous calculations. If so, then the SelectionView() should be called when the error is confirmed. However, this does not happen. The alert window remains rigid, and you cannot continue using the app.
The function ErrorCheck() returns whether there is no error: 0 ; a warning: 1 ; or an error where you should jump back to the menu: 2.
So if there should be an error, then after confirming the Alert window, the window should close itself and you should be sent to the SelectionView.
If I set a breakpoint at the line where the state variable should be changed, then the alert window closes, and you are sent to the SelectionView.
import SwiftUI
struct NearfieldCalibrationView: View {
#StateObject var ErrorviewModel = ErrorViewModel()
var Speech = SpeechModel()
#State var showAlert = false
#State private var Menu: Int? = 0
#State var isActive = false
#ViewBuilder func getView() -> some View {
switch Menu {
case 1:
SelectionView().navigationBarTitle("").navigationBarHidden(true)
case 2:
WMLoadingMeasurementView(Flag: 5)
case 3:
WMLoadingMeasurementView(Flag: 7)
default:
Text("")
}
}
var body: some View {
ZStack {
BackgroundView()
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
VStack {
Spacer().frame(height: 20)
Text("Hello-StackOverFlow").font(Font.custom("Baro Plain", size: 20)).foregroundColor(.white)
Spacer().frame(height: 20)
Group {
NavigationLink(isActive: $isActive, destination: getView) {
Button(action: {
if SpeakerSetup.count != 2 {
self.Menu = 2
self.isActive = true
} else {
self.Menu = 3
self.isActive = true
}
}) {
Text("Start")
}.buttonStyle(OutlineButton())
.simultaneousGesture(TapGesture().onEnded { Speech.Speech(text: "", StopSpeak: true) })
}
Spacer().frame(height: 20)
}
}
}
.onAppear(perform: {
if ErrorviewModel.ErrorCheck() > 0 { showAlert = true }
else {
Speech.Speech(text:"Hello-StackOverflow",StopSpeak: false)
}
})
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
.alert(isPresented: $showAlert) { () -> Alert in
var button: Alert.Button
if ErrorviewModel.ErrorCheck() == 2 { button = Alert.Button.default(Text("zurück zum Menü")) {
self.Menu = 1; self.isActive = true }}
else { button = Alert.Button.default(Text("Ok")) {}}
return Alert(title: Text("Error"), message: Text(ErrorviewModel.ErrorList()), dismissButton: button)
}
}
}
}

SwiftUI: Replacing window dismisses only topmost modal view

I need to show a login screen when the user session is expired. I tried to achieve this by changing the current window:
#main
struct ResetViewHierarchyApp: App {
#StateObject private var state = appState
var body: some Scene {
WindowGroup {
if state.isLoggedIn {
ContentView()
} else {
LogInView()
}
}
}
}
When no modal views are presented then it works fine. If only one modal view is presented, it also works, the modal view is dismissed. But if there are more than one modal views are presented, then the root view is replaced, but only the topmost modal view is dismissed. Here is ContentView:
struct ContentView: View {
#State private var isPresentingSheet1 = false
#State private var isPresentingSheet2 = false
var body: some View {
Text("Hello, world!")
.padding()
Button(action: {
isPresentingSheet1 = true
}, label: {
Text("Present Sheet")
.padding()
}).sheet(isPresented: $isPresentingSheet1) {
sheetView1
}
}
}
private extension ContentView {
var sheetView1: some View {
VStack {
Text("Sheet 1")
.padding()
Button(action: {
isPresentingSheet2 = true
}, label: {
Text("Present Sheet")
.padding()
}).sheet(isPresented: $isPresentingSheet2) {
sheetView2
}
}
}
var sheetView2: some View {
VStack {
Text("Sheet 2")
.padding()
Button(action: {
appState.isLoggedIn = false
}, label: {
Text("Log Out")
.padding()
})
}
}
}
The same happens if I use fullScreenCover instead of sheet.
Does anybody know how to solve this issue, to dismiss all the presented modals at once?
I've solved this issue with UIKit windows:
#StateObject private var state = appState
#State private var contentWindow: UIWindow?
var body: some Scene {
WindowGroup {
EmptyView()
.onAppear {
updateContentWindow(isLoggedIn: state.isLoggedIn)
}.onReceive(state.$isLoggedIn) { isLoggedIn in
updateContentWindow(isLoggedIn: isLoggedIn)
}
}
}
var window: UIWindow? {
guard let scene = UIApplication.shared.connectedScenes.first,
let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate,
let window = windowSceneDelegate.window else {
return nil
}
return window
}
func updateContentWindow(isLoggedIn: Bool) {
contentWindow?.isHidden = true
contentWindow = nil
if let windowScene = window?.windowScene {
contentWindow = UIWindow(windowScene: windowScene)
contentWindow?.windowLevel = UIWindow.Level.normal
if isLoggedIn {
contentWindow?.rootViewController = UIHostingController(rootView: ContentView())
} else {
contentWindow?.rootViewController = UIHostingController(rootView: LogInView())
}
contentWindow?.makeKeyAndVisible()
}
}
It is indeed a strange bug.. however I found a workaround for it.
You can keep your States of the modal View inside your Observable / Environment Object. When logging out, you have to make sure to close all your sheets.
Here is a example:
First adding showSheet as Published Value in the AppState
class AppState : ObservableObject {
#Published var isLoggedIn : Bool = true
#Published var showSheet1 : Bool = false
#Published var showSheet2 : Bool = false
}
When logging out, turn all your sheets to false.
Button(action: {
self.state.isLoggedIn = false
self.state.showSheet1 = false
self.state.showSheet2 = false
}, label: {
Text("Log Out")
.padding()
})
Of course you have to use these values in your Button for toggling sheet and in your sheet.
.sheet(isPresented: $state.showSheet2) {
Edit:
Even simpler, you don't have to manually set it to false in the LogOut action. Instead do it all in the appState
#Published var isLoggedIn : Bool = true {
willSet {
if newValue == false {
showSheet1 = false
showSheet2 = false
}
}
}

SwiftUI: How to show an alert after a sheet is closed?

I'm trying to show an alert which is triggered by a modal sheet. Here's a small demo project:
import SwiftUI
struct ContentView: View {
#State private var showSheet = false
#State private var showAlert = false
var body: some View {
Button("Press") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
Button("Close with alert") {
showSheet = false
showAlert = true
}
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Alert"))
}
}
}
After clicking on the "Press" button, a modal sheet appears with a button "Close with alert". If this button is pressed the sheet closes and nothing happens. I expect to have the alert shown.
It seems that the animation of hiding the sheet is causing the issue as SwiftUI doesn't seem to consider the sheet as closed after setting showSheet = false. The following warning appears which is supporting this theory:
[Presentation] Attempt to present <SwiftUI.PlatformAlertController:
0x7fbbab012200> on
<TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier_:
0x7fbbaa60b7d0> (from
<TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier_:
0x7fbbaa60b7d0>) which is already presenting
<TtGC7SwiftUI22SheetHostingControllerVS_7AnyView: 0x7fbbaa413200>.
You can use onDismiss.
Here are some examples based on when do you want to present an alert:
Always close with an alert:
struct ContentView: View {
#State private var showSheet = false
#State private var showAlert = false
var body: some View {
Button("Press") {
showSheet = true
}
.sheet(isPresented: $showSheet, onDismiss: {
showAlert = true
}) {
Button("Close") {
showSheet = false
}
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Alert"))
}
}
}
Close with an alert on button click only:
struct ContentView: View {
#State private var showSheet = false
#State private var showAlert = false
#State private var closeSheetWithAlert = false
var body: some View {
Button("Press") {
showSheet = true
closeSheetWithAlert = false
}
.sheet(isPresented: $showSheet, onDismiss: {
showAlert = closeSheetWithAlert
}) {
Button("Close") {
closeSheetWithAlert = true
showSheet = false
}
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Alert"))
}
}
}

Trying to push a new view after server response in SwiftUI

I want to show my new view after the success login process with Firebase, my signUp and Recover password are already working because I'm showing it as a sheet but in this one, I want to show a new view, I have tried with NavigationLink, with onReceive but I have been unable to do this work.
struct LoginView: View {
#ObservedObject var viewModel = ViewModel()
#State private var formOffset: CGFloat = 0
#State private var presentSignUpSheet = false
#State private var presentPasswordRecoverySheet = false
#State private var presentLobbySheet = false
var body: some View {
VStack {
HeaderView(title: Constants.appName)
Spacer()
Divider()
Group {
BodyView(value: viewModel).viewSelection(view: Constants.QuestionnaireView.signIn.rawValue)
LCButton(text: Constants.login) {
self.viewModel.signIn()
}.alert(isPresented: $viewModel.thereIsAnError) {
Alert(title: Text(Constants.alert), message: Text(viewModel.errorMessage), dismissButton: .default(Text(Constants.ok)))
}
Button(action: {
self.presentSignUpSheet.toggle()
}) {
Text(Constants.signUp)
}.sheet(isPresented: $presentSignUpSheet) {
SignUpView()
}.padding()
Button(action: {
self.presentPasswordRecoverySheet.toggle()
}) {
Text(Constants.forgotPassword)
}.sheet(isPresented: $presentPasswordRecoverySheet) {
RecoverPasswordView()
}.padding()
}
}.edgesIgnoringSafeArea(.top)
.padding()
.offset(y: self.formOffset)
}
}
class ViewModel: ObservableObject {
#Published var user = User()
#Published var confirmPassword = ""
#Published var thereIsAnError = false
#Published var errorMessage = ""
var viewDismissalModePublisher = PassthroughSubject<Bool, Never>()
var onSuccessLogin = PassthroughSubject<Bool, Never>()
private var shouldPopView = false {
didSet {
viewDismissalModePublisher.send(shouldPopView)
}
}
private var shouldShowLobbyView = false {
didSet {
onSuccessLogin.send(shouldShowLobbyView)
}
}
func registerSuccess() {
self.user.email = ""
self.user.password = ""
self.confirmPassword = ""
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.shouldPopView = true
}
}
func signUpProcess() {
if user.password != confirmPassword {
errorMessage = Constants.passConfirmWrong
thereIsAnError.toggle()
} else {
signUp()
}
}
func signUp() {
Auth.auth().createUser(withEmail: user.email, password: user.password) { (result, error) in
if error != nil {
self.errorMessage = error!.localizedDescription
self.thereIsAnError.toggle()
} else {
self.registerSuccess()
}
}
}
func signIn() {
Auth.auth().signIn(withEmail: user.email, password: user.password) { (result, error) in
if error != nil {
self.errorMessage = error!.localizedDescription
self.thereIsAnError.toggle()
} else {
self.shouldShowLobbyView.toggle()
self.user.email = ""
self.user.password = ""
}
}
}
func recoverPassword() {
Auth.auth().sendPasswordReset(withEmail: user.email) { (error) in
if error != nil {
self.errorMessage = error!.localizedDescription
self.thereIsAnError.toggle()
} else {
self.user.email = ""
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.shouldPopView = true
}
}
}
}
}
I had a similar problem and I found a solution that mimics some navigation
I was going to write the basics here but nothing better then posting the source with explanation.
ViewRouter Tutorial
This consists in a ObservableObject as EnvironmentObject and it allows you to show views in full screen, rather than sheets.
In your case you would have a LoginView and in this view you could open sheets for the Signup and Recover Password views and the LobbyView opening as fullscreen view, like it would in UIKit with the present method

How to prevent two buttons being tapped at the same time in swiftUI?

In swift or objective-c, I can set exclusiveTouch property to true or YES, but how do I do it in swiftUI?
Xcode 11.3
Set exclusiveTouch and isMultipleTouchEnabled properties inside your struct init() or place it in AppDelegate.swift for the whole app:
struct ContentView: View {
init() {
UIButton.appearance().isMultipleTouchEnabled = false
UIButton.appearance().isExclusiveTouch = true
UIView.appearance().isMultipleTouchEnabled = false
UIView.appearance().isExclusiveTouch = true
//OR
for subView in UIView.appearance().subviews {
subView.isMultipleTouchEnabled = false
subView.isExclusiveTouch = true
UIButton.appearance().isMultipleTouchEnabled = false
UIButton.appearance().isExclusiveTouch = true
}
}
var body: some View {
VStack {
Button(action: {
print("BTN1")
}){
Text("First")
}
Button(action: {
print("BTN2")
}){
Text("Second")
}
}
}
}
Can be handled something like below :
struct ContentView: View {
#State private var isEnabled = false
var body: some View {
VStack(spacing: 50) {
Button(action: {
self.isEnabled.toggle()
}) {
Text("Button 1")
}
.padding(20)
.disabled(isEnabled)
Button(action: {
self.isEnabled.toggle()
}) {
Text("Button 2")
}
.padding(50)
.disabled(!isEnabled)
}
}
}