MacOS Cocoa - keep dock icon open when all windows are closed - swift

my application is a listener service that is normally idle, waiting for incoming messages over the network, or for instructions from the user via the Dock icon menus.
however something keeps closing or hiding the Dock icon, unless i keep a window open.
i am working in XCode Swift, with Cocoa with Storyboards, and am developing for MacOS 10.13
i started a new project as above, and edited AppDelegate.swift as follows:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
print( "app did finish launching" )
// Insert code here to initialize your application
for window in NSApp.windows {
print( window.debugDescription )
window.close()
}
DispatchQueue.global(qos: .userInitiated).async {
var seconds = 0
while true {
sleep( 1 )
print( ( seconds % 2 ) == 0 ? "tick" : "tock", seconds )
seconds += 1
}
}
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
print( "app should terminate after last window - set regular policy" )
sender.setActivationPolicy( .regular )
return false
}
}
with no other changes, the application builds and runs, closing one window, however;
1 - the dock icon vanishes as soon as the application loses focus.
2 - after about sixty seconds the application slows down, and the sleep call is taking much longer than 1 second to return.
i need to prevent both of these behaviours, from which two questions follow:
a - where does Apple document these behaviours?
b - what is the correct way to control these behaviours?
in my experience it seems that most MacOS apps stay open in the dock, even though all of their windows are closed. for example Finder, Terminal, Safari, Notes, etc, etc.
what am i not understanding?
edited - i found this answer a week ago
Mac app disappears when window is closed then another app is selected
and i added both of parameters to my application Info, however it would seem i made an error, because i checked again today, and found they were both set to YES.
setting them both to NO appears to answer my question, my application is now working properly, and i will continue to test.
thank you, very much appreciated.

Related

applicationShouldTerminate called unexpectedly in macOS Swift app

Many people appear to have a problem where their AppDelegate's applicationShouldTerminate is never called. I have the opposite problem!
Often (at a guess 20% of the time) when I come back to my Mac in the morning, I discover that something caused my app's applicationShouldTerminate to fire. My app then ran its cleanup code, after which applicationWillTerminate fired. However the app never shut down — it's still marked as running in my dock, and when I click on it there, applicationDidFinishLaunching fires and it starts up. Because it was already running, the dock icon does not bounce.
The logs indicate this seems to only happen when I wrap up for the day and my Mac goes to sleep, possibly only after having been plugged back in after running off its battery.
At first I thought it might be because my Mac was trying to shutdown apps to install a system update but this happens even when there are no updates available. And no other apps on my system have the issue.
The same happens with my app on a friend's Mac.
I do have a "tricky" applicationShouldTerminate to get around run loop issues (nb. I'm using Promises):
var shuttingDown: Bool = false
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
log.debug("applicationShouldTerminate")
if shuttingDown { return .terminateNow }
shuttingDown = true
StuffController.shared.terminateRunningStuff()
.timeout(20) // If it hasn't shutdown within 20 seconds, force it.
.always {
// Tell the app it should now actually terminate.
NSApplication.shared.terminate(self)
}
return .terminateCancel
}
Can anyone suggest a reason my applicationShouldTerminate is firing without the user asking it to quit?
Turns out this is a feature not a bug (lol). In Mac OS X Lion (10.7) Apple introduced a feature called "Automatic Termination" where apps would automatically quit after a while of inactivity.
Note that this is intended to be invisible to the end-user; the app appears to be running in the dock, and should restore itself when needed, as if nothing ever happened.
It can be enabled or disabled via the "Application can be killed to reclaim memory or other extraordinary circumstances" configuration in Xcode (the NSSupportsAutomaticTermination key in an app's Info.plist).

How to detect another instance of a Swift app is already running

I am trying to detect in my Swift app if an instance is already running, and if so, refrain the user from launching another instance. I am using NSRunningApplication class to detect such behavior but am having trouble getting it to work properly. I have read through some of the older posts and know that flock() is a lower-level way to detect so but I would prefer to "do it the Swift way" if possible. Here is a snippet of the code:
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Check if app has already been launched. If so, alert user and terminate the new session
let bundleID = Bundle.main.bundleIdentifier!
if NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).count > 1 {
// warn and terminate
NSApp.terminate(nil)
}
// keep going and finish launching the app
...
}
It appears that the count returned is always of value 1 regardless of how many sessions I am trying to open. Any idea what goes wrong, or am I not calling this check in the right place?
Additionally, my app also allows the user to double-click on a file with my defined file extension to launch the app. I assume that if I can get the above working then it should also work for this scenario as well?
UPDATE:
After reviewing the launch behavior of my app using Activity Monitor, I think I have determined the source of the observed behavior.
Full disclosure, my app creates a new process to launch a console app, and will wait until this process completes and terminates. Updated code snippets below:
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Check if app has already been launched. If so, alert user and terminate the new session
let bundleID = Bundle.main.bundleIdentifier!
let num_instances = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).count
// Debug dialog box to see instances count
let alert = NSAlert()
alert.messageText = "msg"
alert.informativeText = "num instances running: \(num_instances)"
alert.alertStyle = NSAlert.Style.critical
alert.addButton(withTitle: "OK")
alert.runModal()
if num_instances > 1 {
// warn and terminate
NSApp.terminate(nil)
}
// keep going and finish launching the app
let task = Process()
task.launchPath = "myConsoleApp"
task.launch()
task.waitUntilExit()
let status = task.terminationStatus
if status == 0 {
// Done with the console app, exit
...
} else {
// something's wrong; report to user
...
}
}
Now, when the app is first launched, there are two processes created initially: mySwiftApp and myConsoleApp. However, with the presence of the debug dialog box, the mySwiftApp process is terminated after myConsoleApp has launched. As a result, the instance count for mySwiftApp is now 0. Therefore, subsequent launch of mySwiftApp will be successful and now there are two myConsoleApp processes running on the system. It is not clear to me why the presence of the alert box will terminate the mySwiftApp process but I suspect that it sends a termination signal to my app and changes its termination status.
If the debug dialog box is not there, due to the waitUnitlExit() setup, any attempt to launch the second instance will not be seen and essentially got dropped to the floor. Although this is the "desired" behavior since the second instance cannot be launched, it will be nice to have a way to notify the user.
Any idea what's the best way to do so?

Swift MacOS - Is there a way to block access(stop focus) to background apps when my app is opened?

I'm fairly new to Mac Development and now I'm developing a Swift application for desktop. The application consist of a window, which has a webview in it. Is there any possiblity that when my window opens it always stays at top, and you cannot click on any other windows bellow it.
For now I have made it this far with my code:
func applicationWillHide(_ notification: Notification) {
NSApplication.shared().activate(ignoringOtherApps: true)
}
func applicationDidResignActive(_ notification: Notification) {
NSApplication.shared().activate(ignoringOtherApps: true)
}
I'm using applicationWillHide and applicationDidResignActive to bring the window back to the top when a user clicks outside of it, but its still possible to focus on background apps, although they are not at the top. Can this "stealing" of focus somehow be disabled?

Dismissing multiple alerts XCUITesing

I am am writing my XCUITests for my app, and currently struggling in finding the best solutions to dismiss my apps alerts. I have two sets of alerts, one is a notifications alert which pops up at the beginning of a fresh install, the second is a location alert when I navigate to the nearby tab on my app after a fresh install. I have tried using
let alert = app.alerts["\u{201c}VoucherCodes\u{201d} Would Like to Send You Notifcations"].collectionViews.buttons["OK"]
if alert.exists{
alert.tap()
}
but no success, I have also tried using a systemAlertMonitorTokenin my my setUp()
systemAlertMonitorToken = addUIInterruptionMonitorWithDescription(systemAlertHandlerDescription) { (alert) -> Bool in
if alert.buttons.matchingIdentifier("OK").count > 0 {
alert.buttons["OK"].tap()
return true
} else {
return false
}
}
Does anyone have any suggestions or point out where I am going wrong so I can fix this, thanks.
You might find these two questions and their answers to be illuminating:
Xcode 7 UI Testing: Dismiss Push and Location alerts
Xcode 7 UI Testing: how to dismiss a series of system alerts in code

NSApplication keyWindow is nil during applicationDidFinishLaunching

Starting with a blank OS X application project, I add the following code to applicationDidFinishLaunching.
func applicationDidFinishLaunching(aNotification: NSNotification) {
let app = NSApplication.sharedApplication()
guard let window = app.keyWindow else {
fatalError("No keyWindow\n")
}
print(window)
}
At launch I hit the error case because my local window variable is nil. Yet when I show the contents of the app variable, I see a valid value for _keyWindow. Also notice that the blank GUI Window is being displayed on the screen next to the stack dump.
Why does the keyWindow: NSWindow? property return nil in this case?
Thanks
NSApplication's keyWindow property will be nil whenever the app is not active, since no window is focused for keyboard events. You can't rely on it being active when it finished launching because the user may have activated some other app between when they launched your and when it finished launching, and Cocoa is designed to not steal focus.
To some extent, you may be seeing that happen more when launching from Xcode because app activation is a bit strange in that case. But, still, you must not write your applicationDidFinishLaunching() method to assume that your app is active.
What you're seeing in terms of the app's _keyWindow instance variable is, of course, an implementation detail. One can't be certain about what it signifies and you definitely should not rely on it. However, I believe it's basically the app's "latent" key window. It's the window that will be made key again when the app is activated (unless it is activated by clicking on another of its windows).