SwiftUI - Alert displays twice after setting its state variable only once - swift

I want to have this button run a certain command and if it fails, I want it to display an Alert saying it failed. It does this fine except when the alert displays, it displays twice but I only set it once.
Here are the two state variables I use to display the alert:
#State private var alert = false
#State private var alertView = Alert(
title: Text("Well Hello There"),
message: Text("You probably shouldn't be seeing this alert but if you are, hello there! (This is a bug)")
)
And here's my button:
Button(action: {
DispatchQueue.global(qos: .background).async {
if let command = action.command {
let error = Connection.shared.run(command: command)
if error != nil {
self.alertView = Alert(
title: Text("Failed to Run Action"),
message: Text("An error occurred while attempting to \(action.label).")
)
print("Displaying alert") // This only gets printed once
self.alert = true
}
}
}
}) {
Text(action.label)
}.alert(isPresented: self.$alert) {
self.alertView
}

Well I think I have found what the problem is, If you put the alert modifier inside a forEach it actually happens to trigger twice for some reason.
Just bring it out and it works as intended.

What if you set false to self.alert forcibly ?
.alert(isPresented: self.$alert) {
self.alertView
}.onAppear{
self.alert = false
}

It's my experience running on macOS. That might be different from yours.
I had a routine in Alert() closure that would trigger a UI refresh because it alters a state variable. It happened the refresh occurred before "isPresent" being toggled automatically by closing the alert presentation, so the view caught the "isPresent" again during the refresh. I can avoid this either by adding some delay to the routine or, in a safer way, hook the alert modifier to a view that is not affected by the refresh.

Related

Is it possible to start the NavigationSplitView in an expanded state on macOS using SwiftUI?

I like the sidebar to be opened at launch.
However when I build and run the app, this is what I get.
So I need to click on the sidebar icon to show it. This is not the behavior I want. Is it possible to change this?
Somehow, without explicitly setting it in code, the app likes to change the column visibility to .detailOnly at launch. To avoid this behavior, I explicitly set it to .all at onAppear
#State private var columnVisibility =
NavigationSplitViewVisibility.all
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
Text("Side bar")
} detail: {
Text("Main part")
}
.onAppear() {
columnVisibility = .all
}
}

SwiftMessage: how to handle action when user tap on outside of message view?

I'm using SwiftMessage in my project. when the specific message is showing on the screen, I want when the user tap on outside of the message view (anywhere else on message view), some actions happening. how can I do this?
update
I forgot to say I'm using SwiftMessage with one button.
Unfortunately, there isn't a way to add an action on tap outside of the MessageView.
However, if your dimMode is interactive (has tap outside to dismiss enabled) you can easily catch .willHide and .didHide events using eventListeners of SwiftMessages.Config:
let view = MessageView.viewFromNib(layout: .cardView)
var config = SwiftMessages.defaultConfig
config.dimMode = .gray(interactive: false)
config.eventListeners.append { event in
switch event {
case .willHide:
// Handle will hide
case .didHide:
// Handle did hide
default:
break
}
}
SwiftMessages.show(config: config, view: view)
Those events will be triggered on tap outside of the MessageView.
Update: In your particular case where you have a button which have a different action from the tap outside action, you can use something like this:
func showMessageView(buttonHandler: #escaping () -> Void, dismiss: #escaping () -> Void) {
var buttonTapped = false
let view = MessageView.viewFromNib(layout: .cardView)
view.buttonTapHandler = { sender in
buttonHandler()
buttonTapped = true
}
var config = SwiftMessages.defaultConfig
config.dimMode = .gray(interactive: false)
config.eventListeners.append { event in
if !buttonTapped, event == .didHide {
dismiss()
}
}
SwiftMessages.show(config: config, view: view)
}
That way when the button is tapped the dismiss closure will never run.

SwiftUI onMoveCommand actions aren't executed

I'm making a macOS app with SwiftUI, and I would like to offset a SwiftUI view using the arrow keys on the built-in keyboard.
I couldn't find many resources online, but onMoveCommand() appears to be the event handler I need. Upon trying it out, I discovered that the action I specified for onMoveCommand() does not appear to be executed. Here's some code I wrote just to test it out:
struct ContentView: View {
var body: some View {
Text("Hello")
.onAppear() {
print("Appeared!")
}
.onMoveCommand() { (direction) in
print("Moved!")
}
.onTapGesture() {
print("Tapped!")
}
}
}
onMoveCommand() does not print "Moved!" when I press the arrow keys, instead I get the error alert sound played, and nothing is printed. onAppear() successfully prints the "Appeared!" message when the view appears, and onTapGesture() prints "Tapped!" correctly whenever I click the text. This seems to tell me that the basic syntax I got for these view events is correct, but I implemented onMoveCommand() incorrectly.
For now I only want my app to print something to the Xcode console when the arrow keys are pressed, and to be able to distinguish which arrow key was pressed. Can someone please explain what I did wrong?
Keyboard events are handled only by view in focus, so fix is
var body: some View {
Text("Hello")
.focusable() // << here !!
.onAppear() {
print("Appeared!")
}
.onMoveCommand() { (direction) in
print("Moved!")
}
.onTapGesture() {
print("Tapped!")
}
}
Tested with Xcode 11.4 / macOS 10.15.4. Make sure you have turned on keyboard navigation in System Preferences.

Why is it that after showing an NSAlert nothing works?

Why is it that after showing an NSAlert nothing works until I close the NSAlert?
I was trying to print a statement after the display of an NSAlert but print is not working.
Below I have attached my code:
let alert: NSAlert = NSAlert()
alert.messageText = "Hello I am Message text"
alert.informativeText = "i am information"
alert.addButton(withTitle: "OK") // First Button
alert.addButton(withTitle: "Cancel") // 2nd Button
alert.alertStyle = NSAlert.Style.warning
alert.delegate = self
if alert.runModal() == .alertFirstButtonReturn {
print("First Button clicked")
} else {
print("Cancel button clicked")
}
print("after NSAlert >>>>>>> ")
My question is why.
Notice how runModal returns the result of the modal as a NSModalResponse. Code after the line alert.runModal() must be able to access the value that it returns, e.g.
let result = alert.runModal()
print(result)
If the code after runModal were run as soon as the modal is displayed, what would result be? The user has not clicked any buttons on the modal yet, so no one knows!
This is why when runModal is called, code execution kind of just pauses there, at that line, until the user chooses one of the options. runModal is synchronous and blocking.
Compare this with alert.beginSheetModal, which accepts a completionHandler closure, and the modal response is not returned, but passed to the completionHandler. This allows the code after the call to continue to run while the modal is presented, because the code after the call does not have access to the modal response. Only the code in the completionHandler does. beginSheetModal is asynchronous.
If you have something you want to print as soon as the alert is displayed, write it before the runModal call, and (optionally) wrap it in a DispatchQueue.asyncAfter/DispatchQueue.async call, so that your print is asynchronous.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
print("Hello")
}
alert.runModal()
if alert.runModal()
This is executed in application-wide modal session
Here is from doc:
Summary
Runs the alert as an app-modal dialog and returns the constant that
identifies the button clicked. Declaration
open func runModal() -> NSApplication.ModalResponse

Check if NSAlert is currently showing up

I am using an NSAlert to display error messages on the main screen of my app.
Basically, the NSAlert is a property of my main view controller
class ViewController: NSViewController {
var alert: NSAlert?
...
}
And when I receive some notifications, I display some messages
func operationDidFail(notification: NSNotification)
{
dispatch_async(dispatch_get_main_queue(), {
self.alert = NSAlert()
self.alert.messageText = "Operation failed"
alert.runModal();
})
}
Now, if I get several notifications, the alert shows up for every notification. I mean, it shows up with the first message, I click on "Ok", it disappears and then shows up again with the second message etc... Which is a normal behaviour.
What I would like to achieve is to avoid this sequence of error message. I actually only care about the first one.
Is there a way to know if my alert view is currently being displayed ?
Something like alert.isVisible as on iOS's UIAlertView ?
From your code, I suspect that notification is triggered in background thread. In this case, any checks that alert is visible right now will not help. Your code will not start subsequent block execution until first block will finish, because runModal method will block, running NSRunLoop in modal mode.
To fix your problem, you can introduce atomic bool property and check it before dispatch_async.
Objective-C solution:
- (void)operationDidFail:(NSNotification *)note {
if (!self.alertDispatched) {
self.alertDispatched = YES;
dispatch_async(dispatch_get_main_queue(), ^{
self.alert = [NSAlert new];
self.alert.messageText = #"Operation failed";
[self.alert runModal];
self.alertDispatched = NO;
});
}
}
Same code using Swift:
func operationDidFail(notification: NSNotification)
{
if !self.alertDispatched {
self.alertDispatched = true
dispatch_async(dispatch_get_main_queue(), {
self.alert = NSAlert()
self.alert.messageText = "Operation failed"
self.alert.runModal();
self.alertDispatched = false
})
}
}
Instead of run modal you could try
- beginSheetModalForWindow:completionHandler:
source: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSAlert_Class/#//apple_ref/occ/instm/NSAlert/beginSheetModalForWindow:completionHandler:
In the completion handler set the alert property to nil.
And only show the alert if the alert property is nil ( which would be every first time after dismissing the alert).
EDIT : I don't see the documentation say anything about any kind of flag you look for.