Open Another MAC App

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

Test Call

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



Leave a reply



Submit