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"))
}
}
}
Related
I have an app with two defined views. I use NavigationView to move between them. I wanted the alert button to switch users back to Main Menu View and I have even found an answer, but after I used it, it produced the screen like this:
How can I let my app navigate back to the main view from the secondary view in a better way?
The code for the second view (first is just a NavigationLink pointing to the second):
import SwiftUI
struct GameView: View {
#State private var questionCounter = 1
#State var userAnswer = ""
#State private var alertTitle = ""
#State private var gameOver = false
#State private var menuNavigation = false
var body: some View {
NavigationView{
ZStack{
NavigationLink(destination:ContentView(), isActive: $menuNavigation){
Text("")
}
VStack{
Text("Give the answer")
TextField("Give it", text: $userAnswer)
Button("Submit", action: answerQuestion)
}
.alert(isPresented: $gameOver) {
Alert(title: Text(alertTitle),
dismissButton: Alert.Button.default(
Text("Back to menu"), action: {
menuNavigation.toggle()
}
)
)
}
}
}
}
func answerQuestion() {
questionCounter += 1
if questionCounter == 2 {
gameOver.toggle()
alertTitle = "Game Over"
}
}
}
Thanks for your help :)
To achieve what you are expecting, you actually need to add the NavigationView inside ContentView, if that's your main view. Because you navigate from ContentView to GameView, and what you are asking here is how to navigate back.
Applying the concept above, you can just dismiss GameView to go back to the in view.
Here is a sample code to achieve that:
Example of main view:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
GameView()
// This is how you hide the "<Back" button, so the user
// can navigate back only when tapping the alert
.navigationBarHidden(true)
} label: {
Text("Go to game view")
}
}
}
}
Example of game view:
struct GameView: View {
// This variable will dismiss the view
#Environment(\.presentationMode) var presentationMode
#State private var questionCounter = 1
#State var userAnswer = ""
#State private var alertTitle = ""
#State private var gameOver = false
// No need to use this variable
// #State private var menuNavigation = false
var body: some View {
// No need to have a NavigationView
// NavigationView {
// ZStack has no function apparently...
// ZStack{
// No need to have a NavigationLink
// NavigationLink(destination:ContentView(), isActive: $menuNavigation){
// Text("")
//}
VStack {
Text("Give the answer")
TextField("Give it", text: $userAnswer)
Button("Submit", action: answerQuestion)
}
.alert(isPresented: $gameOver) {
Alert(title: Text(alertTitle),
dismissButton: Alert.Button.default(
Text("Back to menu"), action: {
// This is how you go back to ContentView
presentationMode.wrappedValue.dismiss()
}
)
)
}
}
func answerQuestion() {
questionCounter += 1
if questionCounter == 2 {
gameOver.toggle()
alertTitle = "Game Over"
}
}
}
I have found a way to make the suggested approach even easier (without using the whole presentationMode syntax):
struct GameView: View {
#Environment(\.dismiss) var dismiss
#State private var questionCounter = 1
#State var userAnswer = ""
#State private var alertTitle = ""
#State private var gameOver = false
var body: some View {
VStack {
Text("Give the answer")
TextField("Give it", text: $userAnswer)
Button("Submit", action: answerQuestion)
}
.alert(isPresented: $gameOver) {
Alert(title: Text(alertTitle),
dismissButton: Alert.Button.default(
Text("Back to menu"), action: {
dismiss()
}
)
)
}
}
func answerQuestion() {
questionCounter += 1
if questionCounter == 2 {
gameOver.toggle()
alertTitle = "Game Over"
}
}
}
I am just curious for this animation, when i click one button, the origin page will fall down a bit and the new page will raise up with a cross button on top left corner.
Seems it's quite a common behavior, anyone who know is there any official api for this animation ?
This is just two sheets presented, one on top of the other.
Example code:
struct ContentView: View {
#State private var isPresented = false
var body: some View {
VStack {
Text("Content")
Button("Present sheet") {
isPresented = true
}
.sheet(isPresented: $isPresented) {
MainSheet()
}
}
}
}
struct MainSheet: View {
#State private var isPresented = false
var body: some View {
VStack {
Text("Sheet content")
Button("Present another sheet") {
isPresented = true
}
.sheet(isPresented: $isPresented) {
Text("Final content")
}
}
}
}
Result:
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
}
}
}
This should be simple but I am hoping to display an alert when a condition is true.(see below) I have seen lots where you used a button to trigger an alert, but I just want an alert to trigger when a condition is met such as in a simple "If" statement. Which should appear as soon as the code is loaded.
import SwiftUI
struct ContentView: View {
#State private var showingAlert = false
var score = 3
var body: some View {
VStack{
if score == 3 {
showingAlert = true
} .alert(isPresented: $showingAlert) {
Alert(title: Text("Hello SwiftUI!"), message: Text("This is some detail message"), dismissButton: .default(Text("OK")))
}
}
}
You can check if your condition is true in the init() method of the view and then set the initial value of your showingAlert.
struct ContentView: View {
#State private var showingAlert = false
var score = 3
init()
{
//check if condition is true
if (true)
{
self._showingAlert = State(initialValue: true)
}
}
var body: some View {
VStack{
EmptyView()
} .alert(isPresented: self.$showingAlert) {
Alert(title: Text("Hello SwiftUI!"), message: Text("This is some detail message"), dismissButton: .default(Text("OK")))
}
}
}
how to navigate out of a ActionSheet where I can only Pass a Text but not a NavigationLink?
Sample Code:
struct DemoActionSheetNavi: View {
#State private var showingSheet = false
var body: some View {
NavigationView {
Text("Test")
.actionSheet(isPresented: $showingSheet) {
ActionSheet(
title: Text("What do you want to do?"),
message: Text("There's only one choice..."),
buttons: [
.default(Text("How to navigate from here to HelpView???")),
])
}
}
}
}
You would need something like this:
struct DemoActionSheetNavi: View {
#State private var showingSheet = false
#State private var showingHelp = false
var body: some View {
NavigationView {
VStack {
Text("Test")
Button("Tap me") { self.showingSheet = true }
NavigationLink(destination: HelpView(isShowing: $showingHelp),
isActive: $showingHelp) {
EmptyView()
}
}
}
.actionSheet(isPresented: $showingSheet) {
ActionSheet(
title: Text("What do you want to do?"),
message: Text("There's only one choice..."),
buttons: [.cancel(),
.default(Text("Go to help")) {
self.showingSheet = false
self.showingHelp = true
}])
}
}
}
You have another state that programmatically triggers a NavigationLink (you could also do it using .sheet and modal presentation). You would also need to pass showingHelp as a #Binding to help view to be able to reset it.
struct HelpView: View {
#Binding var isShowing: Bool
var body: some View {
Text("Help view")
.onDisappear() { self.isShowing = false }
}
}