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
Related
I'm still learning Swift and therefor don't understand how certain things work - like Placeholders. I installed HGPlaceholders into my project for use with TableViews that I have downloading information from Firebase. I can't seem to figure out how to give the various screens' (there's multiple screens and they each have one "Try Again!" or "Cancel" button) button an action though.
Would someone who is familiar with HGPlaceholders please help me out? How do I give something like
tableView.showLoadingPlaceholder()
an action? Currently it just prints something out to the console, by default.
Yes, I read the documentation for it. Yes, I searched the internet for help. No, I couldn't find anything addressing this question.
Thanks!
HGPlaceholders: HGPlaceholders Documentation
This is what the function that by default prints something when the button in the placeholder is tapped looks like:
#IBAction func sendPlaceholderAction(_ sender: Any) {
onActionButtonTap?()
print("Placeholder action button tapped")
}
I figured it out... took me forever cause I'm stupid.
The actions are handled in this function that you place at the bottom of the class your table view is located in:
extension TestViewController: PlaceholderDelegate {
func view(_ view: Any, actionButtonTappedFor placeholder: Placeholder) {
print(placeholder.key.value)
if placeholder.key.value == "loading" {
print("The placeholder is the Loading screen and Cancel button")
} else if placeholder.key.value == "noResults" {
print("The placeholder is the noResults screen and Try Again button")
} else if placeholder.key.value == "noConnection" {
print("The placeholder is the noConnection screen and Try Again button")
} else if placeholder.key.value == "error" {
print("The placeholder is the error screen and Try Again button")
}
}
}
This is an example of giving an action to each of the four .default options.
Hope this helps anyone else who couldn't figure it out haha!
Side note: One other thing, you have to set your tableView's placeholderDelegate in the viewDidLoad as well.
tableView.placeholderDelegate = self
If you don't, the function will never execute.
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.
I have a button linked to an IBAction that takes around 5-10 seconds to complete.
While the IBAction function is running I want to print progress messages in my view/window.
The only message that prints is the last one, and it prints after the IBAction has completed (ie the button is not blue).
When I step through the function in the debugger, no messages are visible until I'm out of the function (ie button is not blue).
Any idea on how to update the window while the callback is being executed?
embed the IBAction logic inside a background queue:
DispatchQueue.global(qos: .background).async {
// background code
}
and anytime you need to print the progress in your view/window embed that part inside:
DispatchQueue.main.async {
// user interface code
}
generally you may want to try this:
DispatchQueue.global(qos: .background).async {
//background code
DispatchQueue.main.async {
//your main thread
}
}
I've got a NSTextView in my view that works as it should, but when I show a NSAlert and close it, it becomes uneditable (with text still selectable). The NSAlert is a save/cancel alert that updates the textView's string when the user selects save, the string is not updated when the user presses cancel. In both cases the textView was uneditable, the alert is shown when the users has made changes and wants to change the tableView selection.
It feels like the textView refuses first responder but when breaking and checking in the console its "true", I also checked some other values after the view was uneditable:
isEditable was True
isSelectable was True
canBecomeKeyView was True
acceptsFirstResponder was True
acceptsTouchEvents was False, tested True but did not work
My "test" setup:
Video, same when triggering the popup from a tableview selection change and a button : video
My popup code
func dialogOKCancel(question: String, text: String) -> Bool {
let myPopup: NSAlert = NSAlert()
myPopup.messageText = question
myPopup.informativeText = text
myPopup.alertStyle = NSAlertStyle.warning
myPopup.addButton(withTitle: "OK")
myPopup.addButton(withTitle: "Cancel")
return myPopup.runModal() == NSAlertFirstButtonReturn
}
let answer = self.dialogOKCancel(question: "Ok?", text: "Choose your answer.")
also tried:
let a = NSAlert()
a.messageText = "Delete the document?"
a.informativeText = "Are you sure you would like to delete the document?"
a.addButton(withTitle: "Delete")
a.addButton(withTitle: "Cancel")
a.alertStyle = NSAlertStyle.critical
a.beginSheetModal(for: self.view.window!, completionHandler: { (modalResponse) -> Void in
if modalResponse == NSAlertFirstButtonReturn {
print("Document deleted")
}
})
Stuff I tried:
Remove any textView updates, showing the alert still breaks it
Dragging a new "untouched" textView in my storyboard but now both textViews became uneditable
I tried showing the NSAlert when clicking a button instead of when changing the tableView's selection. Here the textView I was editing stays first responder but as soon as Ieave the textView, its uneditable again.
I tried triggering just an animation instead of the NSAlert, here the textView keeps working
I Tried replacing the NSAlert with a overlay View that had a title/description/buttons. when showing this dialog, the textView also became uneditable
I'm stuck on this for a long time and any help is greatly appreciated,
Thanks
After long time of debugging, I found this line to be the one that broke the textfields, I will leave this post online in case someone else stumbles upon this weird problem
window?.styleMask = NSFullSizeContentViewWindowMask
removing this line fixed the problem.
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.