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.
Related
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
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'm trying to get the active tab's page information to be displayed in a popover. I'm able to get it to show the URL or Title of the page currently, but it requires opening and closing the extension popover twice.
I believe it's because to get that information, the below code relies on completionHandler which makes it async.
SFSafariApplication.getActiveWindow { (window) in
window?.getActiveTab { (tab) in
tab?.getActivePage(completionHandler: { (page) in
page?.getPropertiesWithCompletionHandler( { (properties) in
self.pageTitle = properties?.title ?? "Unknown"
self.ActivePageTitle.stringValue = self.pageTitle
})
})
}
}
The first time you open the popover it shows a blank text region, but the second time it will have loaded in the information before and shows it correctly.
I've tried running it in viewDidLoad() but that only fires the first time the popover is opened.
When running it in viewWillAppear() I get the below error:
pid(17738)/euid(501) is calling TIS/TSM in non-main thread environment,
ERROR : This is NOT allowed. Please call TIS/TSM in main thread!!!
I thought maybe switching the extension to use a command instead would work but then realized you can't programatically open the popover window.
Do I have to switch to UITableView or something that has a reloadData() function to run once the async request for data is complete?
MacOS 10.14.4 | Safari 12.1 | Xcode 10.2
Try updating your UI code on the main thread. Wrap the page title updates in DispatchQueue. I had almost the exact same issue, and this worked for me.
SFSafariApplication.getActiveWindow { (window) in
window?.getActiveTab { (tab) in
tab?.getActivePage(completionHandler: { (page) in
page?.getPropertiesWithCompletionHandler( { (properties) in
DispatchQueue.main.async {
self.pageTitle = properties?.title ?? "Unknown"
self.ActivePageTitle.stringValue = self.pageTitle
}
})
})
}
}
To give proper credit, I found the answer while digging through this code on GitHub (check out the updateDataLabels() method):
https://github.com/otzbergnet/tabCount/blob/master/tabCount%20Extension/SafariExtensionViewController.swift
You can get SFSafariPageProperties object in SafariExtensionHandler and use this object in SafariExtensionViewController.
- (void)popoverWillShowInWindow:(SFSafariWindow *)window {
[window getActiveTabWithCompletionHandler:^(SFSafariTab *activeTab) {
[activeTab getActivePageWithCompletionHandler:^(SFSafariPage *page) {
[page getPagePropertiesWithCompletionHandler:^(SFSafariPageProperties *properties) {
// Now you can use "properties" in viewController using shareObject
}];
}];
}];
}
Now need to get properties of SFSafariPage again in viewWillAppear.
I'm currently trying to implement Reachability into my current project. I followed a tutorial on YouTube that worked but I'm unsure whether or not its the correct way of doing it. In the Reachability documentation (https://github.com/ashleymills/Reachability.swift) it shows two examples first one being 'Example - closures' where I assume it's done in the viewDidLoad?
//declare this property where it won't go out of scope relative to your listener
let reachability = Reachability()!
reachability.whenReachable = { reachability in
// this is called on a background thread, but UI updates must
// be on the main thread, like this:
DispatchQueue.main.async {
if reachability.isReachableViaWiFi() {
print("Reachable via WiFi")
} else {
print("Reachable via Cellular")
}
}
}
reachability.whenUnreachable = { reachability in
// this is called on a background thread, but UI updates must
// be on the main thread, like this:
DispatchQueue.main.async {
print("Not reachable")
}
}
do {
try reachability.startNotifier()
} catch {
print("Unable to start notifier")
}
and the last example was 'Example - notifications', this is where I get confused the creator says to do that all in viewDidAppear. Is there really a big difference if I just do everything inside viewDidLoad? Does it change the outcome of anything? It currently works fine but I'm not sure whether it's right, I don't want it affecting me in the future. Any help would be great! Thanks.
It depends on your needs.
If you want to use Reachability...
... dynamically only if this particular view is frontmost, startNotifier() in viewWillAppear and stopNotifier() in viewDidDisappear.
... in this particular view as long as the view is alive/loaded startNotifier() in viewDidLoad.
... globally in all views put the entire code in AppDelegate and post notifications.
I'm trying to disable a button for certain amount of time, but have an issue. My procedure is below:
I have 3 buttons and all buttons are enabled
After one button is clicked, disable all buttons. In the meantime, sending data via Bluetooth...
Enable all buttons after finishing sending data.
My goal is to prevent button click when sending data via Bluetooth. I tried to use Button.userInteractionEnabled = false and Button.enabled = false, but it will go to button action handler(The one that I press during data sending period) again whenever I enable button after finishing sending data. Does any one know how to disable buttons permanently for a certain amount of time?
What you have to do is disable the button upon clicked and then somehow enable it when the data transfer it's done.
If this data transfer is called asynchronously, it will probably have a parameter where you can send in a completion block:
button.isUserInteractionEnabled = false
sendData(data) {
success in
button.isUserInteractionEnabled = true
}
If it doesn't accept a completion block as a parameter, it might work in a different way, such as using notifications (firing a notification with a specific name):
button.isUserInteractionEnabled = false
sendData(data)
// adding the observer that will watch for the fired notification
NotificationCenter.default.addObserver(self, selector: #selector(self.didFinishSendingData(_:)), name: Notification.Name(rawValue: "NOTIFICATION NAME GOES HERE"), object: nil)
func didFinishSendingData(_ notification: Notification?) {
button.isUserInteractionEnabled = true
}
We could definitely help more if you post a sample of your code.
Why can you achieve this on the main thread with an activityIndicator like below:
let activityIndicator = UIActivityIndicatorView()
activityIndicator.frame = view.frame
activityIndicator.center = view.center
activityIndicator.activityIndicatorViewStyle = .gray
activityIndicator.hidesWhenStopped = true
view.addSubview(activityIndicator)
//start activity indicator
activityIndicator.startAnimating()
//send your data via bluetooth on main thread
DispatchQueue.main.async {
//put your sending via bluetooth code here with a completion handler when completes
//then in the completion handler, put below line
activityIndicator.stopAnimating()
}