Crossing the Streams with Services

Brennan Stehling
3 min readAug 27, 2022
Ghostbusters mural at 8th & Bryant in San Francisco

We can do so much with computers these days. Having a Mac with Apple Silicon has kicked it up a few notches. We can break out of the limitations of a web browser and leverage features that only native apps can support. One of these features is services. Your Mac runs many services all the time to support apps and systems.

See for yourself. Open up Activity Monitor and switch to to the Energy tab. Give it a moment and it will list the apps you have been running. Many will have child processes which are the services which support these apps. If you look under Notes you will find it has a service called Notes Networking. It must handle all networking for the app so that if this service needs to be restarted or it crashes it does not crash the Notes app. It can be restarted by the app if the process exits. And app could have multiple services supporting it, like having assistants with specific tasks.

Services are known at XPC Services, or Cross Process Communication. In Xcode you can create a project for a Mac app and then add an extension to that app which is an XPC service. By default it currently offers a project template with Objective-C but it can be done with Swift.

Let’s have a look at how this is done. Below is the Service which is the main entry point for the service when it is launched. It creates a listener and sets the delegate.

Service

Next we can define the delegate which we’ll call ServiceDelegate. We only need to implement a listener function. It is used to handle new connections by defining an interface and exporting an object. We’ll create one called BrowserLauncherService.

Service Delegate

Notice that this source file imports BrowserLaunchKit. This is a module which the app and service extension both import so they can share the same protocol. It is a very simple async function defined below.

Browser Launcher Service

The implementation for this protocol launches a URL in the default browser and logs activity with OSLog so you can view the activity in the Console app. Simply filter to the subsystem and enable info messages in the Console app. For services, the Console app will be a great way to see what services are doing.

Default Browser Launcher Service

Next we will implement the Client which will be run in the app’s main process. It gets the remote service which conforms to the protocol for the service. The Mac app and service extension can use a common dependency which defines this protocol and other shared code which may be helpful.

Client

Now our Mac app can launch a URL in the default browser.

But why?

But why is this useful if I can do this already? Well, services can be registered to run outside a Mac app as Login Items, Launch Agents and Launch Daemons. (see: Service Management) These are services which run even when the app is not running. One of these services could poll a REST API for updates, like GitHub notifications or if a new Xcode beta is available for download and let the user know. Perhaps it would present a notification which the user can tap which would launch the relevant URL to show the user. It would not have to launch its own companion Mac app for this purpose.

If you wanted to have a companion Mac app which is used to control what the services are monitoring it will have support in macOS Ventura to manage the services directly with SMAppService. Currently services can be registered from the command-line which app installers typically use to register their Login Items, Launch Agents or Launch Daemons. If you are now looking to take your iOS apps built with SwiftUI to the Mac this is a great way to leverage this platform in a way that is not possible on the iPhone.

Project: BrowserLauncher

Below are more links to Apple docs which cover many more aspects to running services.

User Agents and Daemons on macOS

--

--