why is #state being reset after alert dismissal - ios

complete SwiftUI beginner here. I'm looking at this example and I'm trying to understand the lifecycle of #state variables.
showingAlert is initialized as false and is set to true when the button is tapped.
the part that I have trouble wrapping my head around is why does it reset back to false when the alert is dismissed? I do not set that to false anywhere.
I expected it to remain true
#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"))
}
}

Because, by definition, if the alert is dismissed, the alert is no longer presented. $showingAlert is a binding — it moves data in both directions. Its value is always correlated to whether the alert is being presented; that's what it means to be a binding.

Related

SwiftUI - Cannot show alert over sheet

The story is a little long. First, let me show some code that doesn't work as I expected.
import SwiftUI
struct ContentView: View {
#State var isPresentingAlert = false
#State var isPresentingSheet = false
var body: some View {
VStack {
Button("Show Sheet") {
isPresentingSheet = true
}
}
.alert("Alert", isPresented: $isPresentingAlert) {}
.sheet(isPresented: $isPresentingSheet) {
Button("Show Alert") {
isPresentingAlert = true
}
}
}
}
The code's intent is clear: the view should display a sheet when user press the "Show Sheet" button, and there is a "Show Alert" button on the sheet, which should cause the alert window to show after tapping. The code is simplified - in the original app, the sheet shows a form to the user, and the alert should show when user submit a form with some invalid data.
But actually when I pressed the "Show Alert" button, the alert wasn't shown. Instead, I got following error in the console:
2023-02-14 23:41:08.163349+0800 SheetAndAlert[8351:217492] [Presentation] Attempt to
present <SwiftUI.PlatformAlertController: 0x15b030600> on
<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__:
0x15b00aa00> (from
<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__:
0x15b00aa00>) which is already presenting
<_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x15b02e000>.
I had no idea what does this error means. Later I changed the code to use UIKIt's UIAlertController, wishing I can get around the error. (The code was adapted from this answer: How do I present a UIAlertController in SwiftUI?)
import SwiftUI
struct ContentView: View {
#State var isPresentingAlert = false
#State var isPresentingSheet = false
var body: some View {
VStack {
Button("Show Sheet") {
isPresentingSheet = true
}
}
.sheet(isPresented: $isPresentingSheet) {
Button("Show Alert") {
ContentView.alertMessage(title: "Alert", message: "Alert")
}
}
}
static func alertMessage(title: String, message: String) {
let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default) { (action: UIAlertAction) in
}
alertVC.addAction(okAction)
let viewController = UIApplication.shared.windows.first!.rootViewController!
viewController.present(alertVC, animated: true, completion: nil)
}
}
But still, it doesn't work. A similiar error is thrown:
2023-02-14 23:56:12.715039+0800 SheetAndAlert[9991:263667] [Presentation] Attempt to
present <UIAlertController: 0x13d80b800> on
<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__:
0x139012600> (from
<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__:
0x139012600>) which is already presenting
<_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x13b02fc00>.
Finally I found a working solution, just move .alert inside .sheet's content:
import SwiftUI
struct ContentView: View {
#State var isPresentingAlert = false
#State var isPresentingSheet = false
var body: some View {
VStack {
Button("Show Sheet") {
isPresentingSheet = true
}
}
.sheet(isPresented: $isPresentingSheet) {
Button("Show Alert") {
isPresentingAlert = true
}
.alert("Alert", isPresented: $isPresentingAlert) {}
}
}
}
Though works, but I still don't understand (at all!) why using UIAlertController cannot solve the problem, while just moving the modifier to another place can.
TL;DR: This behavior is a constraint of the UIKit layer used by SwiftUI and the solution is what you already found, to present the alert from the body of the .sheet modifier.
The behavior you're seeing is caused by the underlying UIKit layer that SwiftUI relies on.
In UIKit, content is managed by UIViewControllers, each one of these view controllers can present another view controller on top of it, but only one. This is often used to present "modals" (or sheets), "popovers" and "alerts".
View controllers are invisible to us when using SwiftUI, but they are still there. Some SwiftUI structs map to some UIKit view controllers, like NavigationView to UINavigationController, and TabView to UITabViewController.
As you may have guessed by now, the .sheet and the .alert modifiers map to their own view controllers. .sheet specifically creates a new view controller that is presented on top of the current view controller.
As I mentioned earlier, view controllers in UIKit can present only one view controller at any time. Because both modifiers are modifying the same content, and hence the same view controller, trying to activate both at the same time will give you that error.
The fix is what you already found: moving the .alert modifier inside the body of .sheet will cause the alert to be presented on the sheet view controller.
If you really need that, I think there is only a bit of hacky solution for that:
.sheet(isPresented: $isPresentingSheet) {
Button("Show Alert") {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isPresentingAlert = true
}
}
}
The other solution that comes to my mind is that you can watch .onChange for isPresentingSheet and call the alert from there (I haven't tested this one it might end up to need asyncAfter too). Obviously you need to add some more variables to know what has happened in your sheet as every dismiss shouldn't call the alert. It is less hacky but more complex to implement and less readable overall.

How to make toggle switch always ON in swift?

I want to make the state of toggle switch to ON always, even if user tries to make it OFF, it should not change. I tried to use .isUserInterstionEnabled = .false, but that didn't work. Can somebody help me on this? Thank you in advance
Try using toggle.isEnabled = false
1.Make the switch outlet in the view controller.
2.Create IBAction of switch and set-:
self.swithCtrl.setOn(true, animated: false)
User will try to disable it but it will remain enable.
There are two ways that I see you can achieve what you want.
Disable it, using the modifier .disabled() (the user will see it slightly faded):
struct Example: View {
#State private var isOn = true
var body: some View {
VStack {
Toggle("Text of toggle", isOn: $isOn)
.disabled(true)
}
}
}
Force it to go back to on, using the modifier .onChange(of:):
struct Example: View {
#State private var isOn = true
var body: some View {
VStack {
Toggle("Text of toggle", isOn: $isOn)
}
.onChange(of: isOn) { _ in
isOn = true
}
}
}

How to use UIAccessibility.post(notification: .layoutChanged, argument: nil) in SwiftUI to move focus to specific view

I have one button on screen and on that button tap it opens one modal (view). but after closing that view, the focus of accessibility voice over goes on top of screen. In UIKit we can use UIAccessibility.post(notification: .layoutChanged, argument: nil) and pass reference as an argument. But how can we achieve this same behaviour in SwiftUI.
How I managed this was using a .accessibilityHidden wrapper on the very top level of the parent view and then used a #State variable as the value to pass into accessibilityHidden. This way the parent view is ignored while the modal is showing. And then reintroduced into the view once the modal is closed again.
struct MainView: View {
#State var showingModal = false
var body: some View {
VStack {
Button(action: {
showingModal = true
}, label: {
Text("Open Modal")
})
.fullScreenCover(isPresented: $showingModal, onDismiss: {
print("Focus coming back to main view")
} content: {
Modal()
})
}
.accessibilityHidden(self.showingModal)
}
}
struct Modal: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("Focus will move here")
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Close Modal to Refocus Back")
}
}
}
}
You can also chain multiple modal / alerts as long at you have #State values to handle the changes to them so the focus moves properly
.accessibilityHidden(self.showingModel1 || self.showingModel2 || self.showingAlert1 || self.showingAlert2)
I know this question is really old, but I literally just was handling this and thought if someone else stumbled onto this question there would be an answer here.

SwiftUI, weird NavigationLink behavior when working with actionSheet

I want to detect if the user meets the prerequisite first before I let him/her in. If the prerequisite is not met, the app will pop an actionSheet and show the user some ways to unlock the feature.
It works perfectly fine when I tap on the text. But when I tap on the blank place on the list. It just skip the Binding. And the weird thing is that in my actually project, the Binding becomes "true" even if I only set it to false.
Here's the question. Am I using the correct approach or did I miss anything? Or is this a bug?
Thank you.
struct ContentView: View {
#State var linkOne = false
#State var linkTwo = false
#State var linkThree = false
#State var actionOne = false
#State var actionTwo = false
#State var actionThree = false
var body: some View {
NavigationView{
List{
NavigationLink("Destination View One", destination: DestOneView(), isActive: self.$linkOne)
.actionSheet(isPresented: self.$actionOne) { () -> ActionSheet in
ActionSheet(title: Text("Hello"), message:Text("This is weird"), buttons: [ActionSheet.Button.cancel()])
}.onTapGesture {
self.actionOne = true
// self.linkOne = true
}
NavigationLink("Destination View Two", destination: DestTwoView(), isActive: self.$linkTwo)
.actionSheet(isPresented: self.$actionTwo) { () -> ActionSheet in
ActionSheet(title: Text("Hello"), message:Text("This is weird"), buttons: [ActionSheet.Button.cancel()])
}
.onTapGesture {
self.actionTwo = true
// self.linkTwo = true
}
NavigationLink("Destination View Three", destination: DestThreeView(), isActive: self.$linkThree)
.actionSheet(isPresented: self.$actionThree) { () -> ActionSheet in
ActionSheet(title: Text("Hello"), message:Text("This is weird"), buttons: [ActionSheet.Button.cancel()])
}
.onTapGesture {
self.actionThree = true
// self.linkThree = true
}
}
}
}
}
Three other views.
struct DestOneView: View {
var body: some View {
Text("First View")
}
}
struct DestTwoView: View {
var body: some View {
Text("Second View")
}
}
struct DestThreeView: View {
var body: some View {
Text("Third View")
}
}
Generally overriding gestures does not work well within the List. One of the solutions can be to use a Button to present a NavigationLink:
import SwiftUI
struct ContentView: View {
#State private var linkOne = false
...
var body: some View {
NavigationView {
List {
NavigationLink(destination: SomeView(), isActive: $linkOne) {
EmptyView()
}
Button(action: {
// here you can perform actions
self.linkOne = true
}, label: {
Text("Some text!")
})
Spacer()
}
}
}
}
When I came back and tested my the code. The solution didn't really work. May be because of the List's bug. People on another post said that the List in the new view only show once if the sheet is inside the List. So I only got an empty List in the new view when I tap the button in Xcode Version 11.5. For some reasion, if I use NavigationView, all contents are shrunk into the middle of the view instead of aligning to the top.
My work around is to set the Binding in .onAppear. It pops the actionSheet when the view loads. And then use the presentationMode method to return to the previous view.
#Environment(\.presentationMode) var presentationMode
.
.
.
ActionSheet.Button.default(Text("Dismiss"), action: {
self.presentationMode.wrappedValue.dismiss()}

How to present a View after tapping an ActionSheet.Button in SwiftUI?

I've been playing around with SwiftUI and got stumped on this simple thing. Basically, I'm trying to trigger a modal after tapping on an ActionSheet.Button. Here's my code so far:
struct SomePage: View {
#State var showSheet = false
var body: some View {
Button(action: {
self.showSheet = true
}) {
Text("Show ation sheet")
}.presentation(sheet)
}
private var sheet: ActionSheet? {
let button = ActionSheet.Button.default(Text("Button") {
self.showSheet = false
// what now??
}
let action = ActionSheet(title: Text("Title"),
message: nil,
buttons: [button])
return showSheet ? action : nil
}
// This is the modal I'm trying to present
// after tapping on the action sheet button
private var modal: Modal {
return Modal(SomePage())
}
}
I've tried adding a second presentation handler to the button and toggling a showModal property but obviously the debugger complained about attempting a second modal presentation while the first one was still being presented.
Does anybody have an idea on how to make this work?
You're not far off from it.
Add another #State to handle the presentation of the modal.
struct ContentView: View {
#State var showSheet = false
#State var showModal = false
var body: some View {
Button(action: {
self.showSheet = true
}) {
Text("Show action sheet")
}
.actionSheet(isPresented: $showSheet, content: actionSheet)
.sheet(isPresented: $showModal, content: { Text("Modal") })
}
private func actionSheet() -> ActionSheet {
let button = ActionSheet.Button.default(Text("Show modal")) {
self.showSheet = false
self.showModal = true
}
let actionSheet = ActionSheet(title: Text("Action Sheet"),
message: nil,
buttons: [button])
return actionSheet
}
}
Result:
Updated for Xcode 11 beta 5

Resources