I have a macOS app which most users use by drag and dropping images on the app icon or my menu bar icon. Some users also use my app through Terminal by running the following commands:
open -a /Users/username/Library/Developer/Xcode/DerivedData/AppName-bomyuotvsgqtachwwiidvpiaktgc/Build/Products/Debug/AppName.app /Users/username/Downloads/image.jpeg
My app handles the link/links being passed in the func application(_ sender: NSApplication, openFiles filenames: [String])
method of AppDelegate.
This works fine so far. If my app is already open, the openFiles
still gets called by MacOS with the new image path and my app opens a new window to display it. This all works well.
Now I want users to be able to pass certain arguments to my app. For example this:
open -a /Users/username/Library/Developer/Xcode/DerivedData/AppName-bomyuotvsgqtachwwiidvpiaktgc/Build/Products/Debug/AppName.app /Users/username/Downloads/image.jpeg --args full
Here I want to receive the full
argument. I read on few other posts to use the CommandLine.arguments
API. But this doesn't seem to contain the arguments. Each time the value of CommandLine.arguments
equals to this:
["/Users/username/Library/Developer/Xcode/DerivedData/AppName-bomyuotvsgqtachwwiidvpiaktgc/Build/Products/Debug/AppName.app/Contents/MacOS/AppName", "-NSDocumentRevisionsDebugMode", "YES"]
I think this is because the CommandLine.arguments
only works when the app is initially launched and arguments are passed into the main
function. But for an already opened app, these don't get passed as main
isn't called again for an already running app.
How do I achieve this?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With