NSWorkspace on macOS: Launching Apps and Monitoring Their Lifecycle
Launch and observe macOS applications with NSWorkspace completion handlers, workspace notifications, coverage caveats, and process-identity checks.
NSWorkspace can launch other applications, open URLs, and report selected changes in the user environment. A launch request is asynchronous and has several distinct milestones: the request was accepted, an NSRunningApplication object became available, the app finished launching, and the app performed the task your product wanted. Treat those as separate states. An app-launch callback is not proof that a document opened correctly or that a background operation completed.
Use NSWorkspace when interacting with user applications as applications. Use Process for a subprocess that your app owns and whose input, output, and exit status are part of your contract. Launching a GUI app through a shell command and trying to infer its lifecycle from Process is usually the wrong abstraction.
Launch with a configuration and completion handler
Use openApplication(at:configuration:completionHandler:) to request a launch from an app URL. Configure activation behavior intentionally. The completion handler reports the running application or an error, and should be handled without blocking the main thread. Retain any request context needed to connect that callback to the document or user action that initiated it.
import AppKit
func openEditor(at applicationURL: URL, with documentURL: URL) {
let workspace = NSWorkspace.shared
let configuration = NSWorkspace.OpenConfiguration()
configuration.activates = true
workspace.openApplication(at: applicationURL, configuration: configuration) {
application, error in
if let error {
reportLaunchFailure(error)
return
}
guard let application else {
reportLaunchFailure(nil)
return
}
openDocumentInLaunchedApp(documentURL, processID: application.processIdentifier)
}
}
func reportLaunchFailure(_ error: Error?) { }
func openDocumentInLaunchedApp(_ url: URL, processID: pid_t) { }
This sample launches an application but the final openDocumentInLaunchedApp function is deliberately a placeholder. If the app should open a document, use the appropriate NSWorkspace.open overload with the desired app and configuration rather than assuming launch alone will pass the document. Validate the resulting callback and document-open outcome separately.
Avoid selecting an app by a display name when a bundle URL or identifier is part of the product contract. Names can be localized or duplicated. Resolve the intended application through an explicit policy, verify it exists, and handle a missing or moved app without launching an unintended substitute.
Treat NSWorkspace.OpenConfiguration as request configuration, not as a guarantee about the other application’s user interface. For example, activates = false asks not to bring the app forward, but the target may already be running, may have its own activation policy, or may present UI as part of its own work. Test the behavior your product actually needs instead of inferring it from the launch callback. If a request should open a particular document, use the workspace API that includes the document and target application; do not launch first and race a second unrelated open request against startup.
Keep a correlation record for each launch intent: the selected bundle URL or identifier, the initiating document/action, request time, and the resulting process identifier when supplied. This makes support diagnostics distinguish “could not locate the target app” from “launch request failed” and “application launched, but the follow-on open operation failed.” Avoid retaining the entire NSRunningApplication indefinitely as a substitute for that record. Re-check liveness before performing later work, and do not assume process identifiers are globally unique over time.
Observe with the workspace notification center
Workspace lifecycle notifications are posted through NSWorkspace.shared.notificationCenter, not necessarily the app’s ordinary notification center. Register on the correct center and remove the observer token when the monitor owner ends. The notification object is the shared workspace, and launch-related user info can include an NSRunningApplication describing the affected app.
import AppKit
final class ApplicationMonitor {
private var launchObserver: NSObjectProtocol?
func start() {
let workspace = NSWorkspace.shared
launchObserver = workspace.notificationCenter.addObserver(
forName: NSWorkspace.didLaunchApplicationNotification,
object: workspace,
queue: .main
) { notification in
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey]
as? NSRunningApplication else { return }
recordLaunch(bundleID: app.bundleIdentifier,
processID: app.processIdentifier)
}
}
func stop() {
if let launchObserver {
NSWorkspace.shared.notificationCenter.removeObserver(launchObserver)
self.launchObserver = nil
}
}
}
func recordLaunch(bundleID: String?, processID: pid_t) { }
The notification is useful for ordinary visible applications, but it does not cover every possible process. Apple’s documentation explicitly says the did-launch notification is not posted for background apps or apps with the LSUIElement key. If the product needs to observe all changes to runningApplications, use the documented observation mechanism for that property and verify its behavior for the deployment target. Do not promise exhaustive process monitoring from this notification alone.
Distinguish launch, activation, and termination
Workspace exposes notifications for launch, activation, deactivation, hiding, and termination. These report different events. An app can launch without becoming frontmost, can activate without a new launch, and can terminate after your observer was removed. Model the event type rather than reducing all of them to a single “app state changed” callback.
NSRunningApplication represents a running app and exposes identity and lifecycle-related properties. A process identifier can be reused after an app exits, so do not use a bare PID as a permanent identity. Pair it with bundle identifier and a launch generation or observation timestamp. If a later operation needs to communicate with that app, verify it is still running and use an appropriate interprocess communication contract rather than assuming the earlier object is still live.
A termination notification means the process finished executing, not that it completed a particular document operation successfully. To determine whether a file was saved, observe the file or use a documented app integration protocol. Never use app termination as a surrogate acknowledgment for business work.
Do not block waiting for the other app
The completion handler returns control asynchronously. Avoid spinning until isFinishedLaunching or sleeping the current thread after a launch request. Such waits can freeze your UI and still fail if the other app takes a long time, shows a dialog, or has an existing process that changes launch behavior.
If the product needs to open a document, pass the document as part of the workspace request and handle its completion or error. If it needs the other application to perform a richer operation, use a supported protocol such as URL schemes, Apple events, or an XPC/service contract where appropriate. Keep application launch separate from the downstream request/response protocol.
Activation is user-visible behavior. Set it only when the person initiated a workflow that should bring the target app forward. Background utility launches should not unexpectedly steal focus. Respect the target app’s own activation policy and avoid toggling hide/show state to force a visual outcome that the user did not request.
Observer ownership and notification races
Register observers before initiating an operation if the event ordering matters, but still account for races between current-state snapshots and notification delivery. A process might launch between your initial enumeration and observer registration. A robust monitor registers, enumerates current applications, then reconciles duplicate observations by stable identity. Keep handlers short and move slow analysis to a separate task.
Make reconciliation idempotent. A notification may describe a process that is already present in the snapshot, so key the record by an identity that combines the bundle identifier when available, process identifier, and the current launch generation/time. A missing bundle identifier should remain a valid observation rather than causing the event to be dropped. Expire records when the corresponding process terminates, and guard deferred work against a stale record whose PID has since been reused.
Be precise about what is being monitored. NSWorkspace describes application-level events, not every executable, helper, daemon, or child process on the machine. A product that needs a complete process inventory or security telemetry requires a different supported interface and a separately reviewed privacy model. Do not poll runningApplications at a high frequency to compensate for a notification that intentionally does not cover the target class; that adds overhead without establishing a complete audit trail.
Do not retain a monitor forever just because a notification center retains an observer token. Store the token and remove it when the feature stops. If a callback captures a document controller, avoid a cycle that prevents document closure. Route all notifications through one owner so multiple windows do not each process the same event and duplicate side effects.
Failure and acceptance matrix
Test a valid app URL, missing app bundle, permission or launch failure, existing app instance, activation disabled, app that takes a long time to launch, app termination immediately after launch, background-only app, LSUIElement app, and document open failure after a successful launch. Assert the user-visible state distinguishes launch failure from downstream task failure and no blocking wait occurs on the main thread.
For notification monitoring, test registration on NSWorkspace.shared.notificationCenter, observer removal, process launch during startup enumeration, app activation without launch, app termination before a deferred callback, and duplicate observations. Check bundle ID, PID, and event name while avoiding collection of unrelated process details without product need.
Also test app relocation between discovery and launch, a bundle URL that resolves to a different version than expected, multiple installed copies of the same bundle identifier, an already-running application, and a request that succeeds while the intended document does not open. Define an explicit selection rule for duplicate installations and surface an actionable error when that rule cannot be applied. If the user can cancel the initiating workflow while a launch is pending, invalidate its correlation token so a late completion does not reopen a closed window or attach to a newer document session.
Use NSWorkspace for system-mediated application launch and user app lifecycle events. The application still owns request identity, document handoff, observer coverage limits, focus policy, and confirmation of the downstream work. A launch event is a milestone, not a transaction receipt.
Related:
- AppKit Application Lifecycle on macOS: Launch, Open Events, and Termination
- Foundation Process on macOS: Subprocess Ownership, Pipes, and Exit State
Sources: