MacOS app - why does CommandLine.arguments of an already open app not contain the passed --args arguments?
With the -n option of the open
command, you would always get a new instance of the app that could receive the arguments this way. However, you probably don`t want to have multiple instances of your app.
This means that you need a small command line program that performs a request with the arguments if the app is already running. In case the app is not running yet, it should probably start the app with the arguments.
There are several ways to communicate between a command line program and a GUI app. Depending on the exact requirements, they have corresponding advantages and disadvantages.
Command Line Tool
However, the actual process is always the same. Here exemplary with the help of the DistributedNotificationCenter, there the command line tool could look like this:
import AppKit
func stdError(_ msg: String) {
guard let msgData = msg.data(using: .utf8) else { return }
FileHandle.standardError.write(msgData)
}
private func sendRequest(with args: [String]) {
if let json = try? JSONEncoder().encode(args) {
DistributedNotificationCenter.default().postNotificationName(Notification.Name(rawValue: "\(bundleIdent).openRequest"),
object: String(data: json, encoding: .utf8),
userInfo: nil,
deliverImmediately: true)
}
}
let bundleIdent = "com.software7.test.NotificationReceiver"
let runningApps = NSWorkspace.shared.runningApplications
let isRunning = !runningApps.filter { $0.bundleIdentifier == bundleIdent }.isEmpty
let args = Array(CommandLine.arguments.dropFirst())
if(!isRunning) {
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdent) {
let configuration = NSWorkspace.OpenConfiguration()
configuration.arguments = args
NSWorkspace.shared.openApplication(at: url,
configuration: configuration,
completionHandler: { (app, error) in
if app == nil {
stdError("starting \(bundleIdent) failed with error: \(String(describing: error))")
}
exit(0)
})
} else {
stdError("app with bundle id \(bundleIdent) not found")
}
} else {
sendRequest(with: args)
exit(0)
}
dispatchMain()
Note: since DistributedNotificationCenter can no longer send the userInfo with current macOS versions, the arguments are simply converted to JSON and passed with the object parameter.
App
The actual application can then use applicationDidFinishLaunching to determine if it was started freshly with arguments. If so, the arguments are evaluated. It also registers an observer for the notification. When a notification is received, the JSON is converted to the arguments. Here they are simply displayed in an alert in both cases. Could look like this:
import Cocoa
@main
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
let bundleIdent = "com.software7.test.NotificationReceiver"
DistributedNotificationCenter.default().addObserver(self,
selector: #selector(requestReceived(_:)),
name: Notification.Name(rawValue: "\(bundleIdent).openRequest"),
object: nil)
let args = Array(CommandLine.arguments.dropFirst())
if !args.isEmpty {
processArgs(args)
}
}
private func processArgs(_ args: [String]) {
let arguments = args.joined(separator: "\n")
InfoDialog.show("request with arguments:", arguments)
}
@objc private func requestReceived(_ request: Notification) {
if let jsonStr = request.object as? String {
if let json = jsonStr.data(using: .utf8) {
if let args = try? JSONDecoder().decode([String].self, from: json) {
processArgs(args)
}
}
}
}
}
struct InfoDialog {
static func show(_ title: String, _ info: String) {
let alert = NSAlert()
alert.messageText = title
alert.informativeText = info
alert.alertStyle = .informational
alert.addButton(withTitle: "OK")
alert.runModal()
}
}
As mentioned earlier, the appropriate method of inter-process communication should be chosen depending on the exact requirements, but the flow would always be roughly the same.
Test
Mac app disappears when window is closed then another app is selected
This behavior is known as Automatic Termination. I find it a misfeature, but Apple considers it a feature.
Your app may not have actually quit. It may just appear to have quit. "Launching" it again will just make it reappear in the Dock. It's also possible that some apps which look like they're still running have actually been terminated by the system. In theory, if you try to switch to them, they will be launched and told to restore their previous state to maintain the illusion that they were running all along. In practice, apps (even Apple's) rarely properly restore things to exactly how they were.
The process list in Activity Monitor is a true reflection for what is and is not actually running. Look there to determine if your app has really been terminated.
A developer is supposed to have to opt-in to Automatic Termination because it requires explicit coding of state restoration. However, Xcode's app project/target templates have it enabled by default. You can remove the NSSupportsAutomaticTermination
key from your Info.plist to disable it.
Likewise, you'll presumably want to disable Sudden Termination, too, if you're not prepared to support it. You would remove the NSSupportsSuddenTermination
key.
Related Topics
Is There a Daylight Savings Check in Swift
Extend Generic Array<T> to Adopt Protocol
Using Applescript with Apple Events in MACos - Script Not Working
How to Implement iOServicematchingcallback in Swift
Instance Member Cannot Be Used on Type Class
Swift/Firebase - Sort Posts in Tableview by Date
Difference Between Nsrange and Nsmakerange
Swift Task Continuation Misuse: Leaked Its Continuation - for Delegate
Possible to Pass an Enum Type Name as an Argument in Swift
Swift Operator "*" Throwing Error on Two Ints
Safe to Signal Semaphore Before Deinitialization Just in Case
How to Create Uicollectionviewcell Programmatically
How to Change Navigationbar Font in Swift
Libsqlite3.Dylib and Libz.Dylib Missing in Xcode 7. How to Use Parse
How to Get the Current Queue Name in Swift 3
Swift 5 Table View Cell with Uiimage Appears Very Tall and Image Extremely Zoomed