I'm developing a macOS application that tracks the duration of a user's session using a timer, which is displayed both in the main window and in an menu bar extra view. I have a couple of questions regarding the timer's behavior:
What happens to the timer if the user closes the application's window (causing the app to become inactive) but does not fully quit it? Does the timer continue to run, pause, or behave in some other way?
Will the app nap feature stop the timer when app is in-active state?
Processes & Concurrency
RSS for tagDiscover how the operating system manages multiple applications and processes simultaneously, ensuring smooth multitasking performance.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Hi,
I’m using a Local Push Connectivity Extension and encountering an issue with DispatchSourceTimer.
In my extension, I create a DispatchSourceTimer that is supposed to fire every 1 second. It works as expected at first. However, when the app is in the foreground and the device is locked, the timer eventually stops firing after 1–3 hours.
The extension process is still alive, and no errors are thrown
Has anyone experienced this behavior?
Is this a known limitation for timers inside NEAppPushProvider, or is the extension being deprioritized silently by the system?
Any insights or suggestions would be greatly appreciated.
Thanks!
I have followed these steps as mentioned in this link :(https://developer.apple.com/forums/thread/721737)
My projects app bundle structure is like this :
TWGUI.app
TWGUI.app/Contents
TWGUI.app/Contents/_CodeSignature
TWGUI.app/Contents/_CodeSignature/CodeResources
TWGUI.app/Contents/MacOS
TWGUI.app/Contents/MacOS/TWAgent
TWGUI.app/Contents/MacOS/TWGUI
TWGUI.app/Contents/Resources
TWGUI.app/Contents/Library
TWGUI.app/Contents/Library/LaunchAgents
TWGUI.app/Contents/Library/LaunchAgents/com.example.TWGUI.agent.plist
TWGUI.app/Contents/Info.plist
TWGUI.app/Contents/PkgInfo
TWGUI is my main GUI App , i which i want to embed TWAgent (a command line tool target) and register it using SMAppServices so that launchd can launch it.
In TWGUI, code for registering to launchd using SMAppServices is structure as follow :
import SwiftUI
import ServiceManagement
struct ContentView: View {
let agent = SMAppService.agent(plistName: "com.example.TWGUI.agent.plist")
var body: some View {
VStack {
Button("Register Agent") {
RegisterAgent ()
}
.padding()
Button("Unregister Agent") {
UnregisterAgent ()
}
.padding()
}
}
func RegisterAgent() {
DispatchQueue.global(qos: .background).async {
do {
print("Registering Agent. Status: \(agent.status.rawValue)")
try agent.register()
print("Agent registered")
} catch {
print("Failed to register agent: \(error)")
}
}
}
func UnregisterAgent() {
DispatchQueue.global(qos: .background).async {
do {
print("Unregistering Agent. Status: \(agent.status.rawValue)")
try agent.unregister()
print("Agent unregistered")
} catch {
print("Failed to unregister agent: \(error)")
}
}
}
}
com.example.TWGUI.agent.plist :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs$
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.TWGUI.agent</string>
<key>ProgramArguments</key>
<array>
<string>Contents/MacOS/TWAgent</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
I have used ProgramArguements instead of using Program in above plist because i was getting this error when i was using Program earlier :
Registering Agent. Status: 3
Failed to register agent: Error Domain=SMAppServiceErrorDomain Code=111 "Invalid or missing Program/ProgramArguments" UserInfo={NSLocalizedFailureReason=Invalid or missing Program/ProgramArguments}
TWGUI apps Info.plist is :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>23C71</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>TWGUI</string>
<key>CFBundleIdentifier</key>
<string>com.example.TWAgent</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>TWGUI</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string></string>
<key>DTPlatformName</key>
<string>macosx</string>
<key>DTPlatformVersion</key>
<string>14.2</string>
<key>DTSDKBuild</key>
<string>23C53</string>
<key>DTSDKName</key>
<string>macosx14.2</string>
<key>DTXcode</key>
<string>1510</string>
<key>DTXcodeBuild</key>
<string>15C65</string>
<key>LSMinimumSystemVersion</key>
<string>14.2</string>
</dict>
</plist>
TWAgent target has main.swift file which does this :
import Foundation
let startTime = CFAbsoluteTimeGetCurrent()
func logTimeSinceStart() {
let elapsedTime = CFAbsoluteTimeGetCurrent() - startTime
NSLog("Time since program started: \(elapsedTime) seconds")
}
func startLoggingTime() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
logTimeSinceStart()
}
}
// Start logging time
startLoggingTime()
// Keep the run loop running
CFRunLoopRun()
I followed these exact same steps in another project earlier and my agent was getting registered, although i lost that project due to some reasons.
But now i am getting this error when i am registering or unregistering agent using SMAppServices from the code above :
Registering Agent. Status: 3
Failed to register agent: Error Domain=SMAppServiceErrorDomain Code=1 "Operation not permitted" UserInfo={NSLocalizedFailureReason=Operation not permitted}
I tried diffrent fixes for like this :
Moved app bundle to /applications folder
Gave permission for full disc access to this app .
Code sign again (both agent and TWGUI
...
But nothing seems to work , getting same error.
I tried to launch agent using :
Launchctl load com.example.TWGUI.agent.plist
and it worked , so there is no issue with my plist implementation.
Can someone help me understand how can i solve this issue ? or if i am following right steps ? Can give steps need to follow to implement this and steps so that i can register and start my agent using SMAppServices?
And i also tried the project give in apples official documentation : [https://developer.apple.com/documentation/servicemanagement/updating-your-app-package-installer-to-use-the-new-service-management-api)
but got same error in this project as well .
I'm working on implementing file moving with NSFileCoordinator. I'm using the slightly newer asynchronous API with the NSFileAccessIntents. My question is, how do I go about notifying the coordinator about the item move? Should I simply create a new instance in the asynchronous block? Or does it need to be the same coordinator instance?
let writeQueue = OperationQueue()
public func saveAndMove(data: String, to newURL: URL) {
let oldURL = presentedItemURL!
let sourceIntent = NSFileAccessIntent.writingIntent(with: oldURL, options: .forMoving)
let destinationIntent = NSFileAccessIntent.writingIntent(with: newURL, options: .forReplacing)
let coordinator = NSFileCoordinator()
coordinator.coordinate(with: [sourceIntent, destinationIntent], queue: writeQueue) { error in
if let error {
return
}
do {
// ERROR: Can't access NSFileCoordinator because it is not Sendable (Swift 6)
coordinator.item(at: oldURL, willMoveTo: newURL)
try FileManager.default.moveItem(at: oldURL, to: newURL)
coordinator.item(at: oldURL, didMoveTo: newURL)
} catch {
print("Failed to move to \(newURL)")
}
}
}
Topic:
App & System Services
SubTopic:
Processes & Concurrency
Tags:
Files and Storage
Swift
iCloud Drive
Concurrency
Hi, I have some questions regarding the Background Assets Extension and DeviceCheck framework.
Goal: Ensure that only users who have purchased the app can access the server's API without any user authentication using for example DeviceCheck framework and within a Background Assets Extension.
My app relies on external assets, which I'm loading using the Background Assets Extension. I'm trying to determine if it's possible to obtain a challenge from the server and send a DeviceCheck assertion during this process within the Background Assets Extension.
So far, I only receive session-wide authentication challenges—specifically NSURLAuthenticationMethodServerTrust in the Background Assets Extensio. I’ve tested with Basic Auth (NSURLAuthenticationMethodHTTPBasic) just for experimentation, but the delegate
func backgroundDownload(
_ download: BADownload,
didReceive challenge: URLAuthenticationChallenge
) async -> (URLSession.AuthChallengeDisposition, URLCredential?)
is never called with that authentication method. It seems task-specific challenges aren't coming through at all.
Also, while the DCAppAttestService API appears to be available on macOS, DCAppAttestService.isSupported always returns false (in my testing), which suggests it's not actually supported on macOS. Can anyone confirm if that’s expected behavior?
Topic:
App & System Services
SubTopic:
Processes & Concurrency
Tags:
Network
DeviceCheck
Background Assets
Hello!
I'm writing a System Extension that is an Endpoint Security client. And I want to Deny/Allow executing some XPC Service processes (using the ES_EVENT_TYPE_AUTH_EXEC event) depending on characteristics of a process that starts the XPC Service.
For this purpose, I need an API that could allow me to obtain an execution context of the XPC Service process. I can obtain this information using the "sudo launchctl procinfo <pid>" command (e.g. I can use the "domain = pid/3428" part of the output for this purpose). Also, I know that when the xpcproxy process is started, it gets as the arguments a service name and a pid of the process that requests the service so I can grasp the execution context from xpcproxy launching. But are these ways to obtain this info legitimate?
When I search, it's always people trying to do stuff in the background. I want my app to only do stuff when it is active. And this post https://developer.apple.com/forums/thread/685525 seems to have prevented replies from the start. Which means it's just a documentation page and does not belong in the discussion forums at all, because it prevents all discussion.
I am building a Mac app that launch a GUI helper app and use XPC to communicate between them.
Main app start a XPC Listener using NSXPCListener(machServiceName: "group.com.mycompany.myapp.xpc")
Launch the helper app
Helper app connect to the XPC service and listen command from main app.
What I observe is the app seems can start XPC listener while I run it via Xcode. If I run the app using TestFlight build, or via the compiled debug binary (same one that I use on Xcode), it cannot start the XPC service. Here is what I see in the Console:
[0x600000ef7570] activating connection: mach=true listener=true peer=false name=group.com.mycompany.myapp.xpc
[0x600000ef7570] listener failed to activate: xpc_error=[1: Operation not permitted]
Both main app and helper app are sandboxed and in the same App Group - if they were not, I cannot connect the helper app to main app. I can confirm the entitlement profiles did contain the app group.
If I start the main app via xcode, and then launch the helper app manually via Finder, the helper app can connect to the XPC and everything work.
It is not related to Release configuration, as the same binary work while I am debugging, but not when I open the binary manually.
For context, the main app is a Catalyst app, and helper app is an AppKit app. To start a XPC listener on Catalyst, I had do it in a AppKit bridge via bundle.
Given the app worked on Xcode, I believe this approach can work. I just cannot figure out why it only work while I am debugging.
Any pointer to debug this issue is greatly appreciated. Thanks!
Will App be terminated or suspended when updating to incremental version from app store?
Topic:
App & System Services
SubTopic:
Processes & Concurrency
Tags:
App Store
Core Location
Background Tasks
We are currently developing a VoIP application that supports Local Push extention.
I would like to ask for your advice on how the extension works when the iPhone goes into sleep mode.
Our App are using GCD (Grand Central Dispatch) to perform periodic processing within the extension, creating a cycle by it.
[sample of an our source]
class LocalPushProvider: NEAppPushProvider {
let activeQueue: DispatchQueue = DispatchQueue(label: "com.myapp.LocalPushProvider.ActiveQueue", autoreleaseFrequency: .workItem)
var activeSchecule: Cancellable?
override func start(completionHandler: @escaping (Error?) -> Void) {
:
self.activeSchecule = self.activeQueue.schedule(
after: .init(.now() + .seconds(10)), // start schedule after 10sec
interval: .seconds(10) // interval 10sec
) {
self.activeTimerProc()
}
completionHandler(nil)
}
}
However In this App that we are confirming that when the iPhone goes into sleep mode, self.activeTimerProc() is not called at 10-second intervals, but is significantly delayed (approximately 30 to 180 seconds).
What factors could be causing the timer processing using GCD not to be executed at the specified interval when the iPhone is in sleep mode?
Also, please let us know if there are any implementation errors or points to note.
I apologize for bothering you during your busy schedule, but I would appreciate your response.
Topic:
App & System Services
SubTopic:
Processes & Concurrency
Tags:
PushKit
CallKit
Network Extension
Dispatch
General:
DevForums subtopic: App & System Services > Processes & Concurrency
Processes & concurrency covers a number of different technologies:
Background Tasks Resources
Concurrency Resources — This includes Swift concurrency.
Service Management Resources
XPC Resources
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Topic:
App & System Services
SubTopic:
Processes & Concurrency
Hi, I'm working on an application on MacOS. It contains a port-forward feature on TCP protocol.
This application has no UI, but a local HTTP server where user can access to configure this application.
I found that my application always exit for unknown purpose after running in backgruond for minutes. I think this is about MacOS's background process controlling.
Source codes and PKG installers are here: https://github.com/burningtnt/Terracotta/actions/runs/16494390417
My app does really large uploads. Like several GB. We use the AWS SDK to upload to S3.
It seemed like using BGContinuedProcessingTask to complete a set of uploads for a particular item may improve UX as well as performance and reliability.
When I tried to get BGContinuedProcessingTask working with the AWS SDK I found that the task would fail after maybe 30 seconds. It looked like this was because the app stopped receiving updates from the AWS upload and the task wants consistent updates. The AWS SDK always uses a background URLSession and this is not configurable. I understand the background URLSession runs in a separate process from the app and maybe that is why progress updates did not continue when the app was in the background.
Is it expected that BGContinuedProcessingTask and background URLSession are not really compatible? It would not be shocking since they are 2 separate background APIs.
Would the Apple recommendation be to use a normal URLSession for this, in which case AWS would need to change their SDK?
Or does Apple think that BGContinuedProcessingTask should just not be used with uploads? In other words use an upload specific API.
Thanks!
Topic:
App & System Services
SubTopic:
Processes & Concurrency
Tags:
iOS
Beta
Background Tasks
CFNetwork
Hello,
I am trying to implement a subscriber which specifies its own demand for how many elements it wants to receive from a publisher.
My code is below:
import Combine
var array = [1, 2, 3, 4, 5, 6, 7]
struct ArraySubscriber<T>: Subscriber {
typealias Input = T
typealias Failure = Never
let combineIdentifier = CombineIdentifier()
func receive(subscription: any Subscription) {
subscription.request(.max(4))
}
func receive(_ input: T) -> Subscribers.Demand {
print("input,", input)
return .max(4)
}
func receive(completion: Subscribers.Completion<Never>) {
switch completion {
case .finished:
print("publisher finished normally")
case .failure(let failure):
print("publisher failed due to, ", failure)
}
}
}
let subscriber = ArraySubscriber<Int>()
array.publisher.subscribe(subscriber)
According to Apple's documentation, I specify the demand inside the receive(subscription: any Subscription) method, see link.
But when I run this code I get the following output:
input, 1
input, 2
input, 3
input, 4
input, 5
input, 6
input, 7
publisher finished normally
Instead, I expect the subscriber to only "receive" elements 1, 2, 3, 4 from the array.
How can I accomplish this?
On MacOS 26 Tahoe, we are getting a background warning message stating, “App is running in the background…”
Is this expected behavior on the new OS?
Thanks
Asutos
Topic:
App & System Services
SubTopic:
Processes & Concurrency
Hi all,
I’ve built an Electron application that uses two child processes:
An Express.js server
A Python executable (packaged .exe/binary)
During the development phase, everything works fine — the Electron app launches, both child processes start, and the app functions as expected.
But when I create a production build for macOS, the child processes don’t run.
Here’s a simplified snippet from my electron.mjs:
import { app, BrowserWindow } from "electron";
import { spawn } from "child_process";
import path from "path";
let mainWindow;
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true,
},
});
mainWindow.loadFile("index.html");
// Start Express server
const serverPath = path.join(process.resourcesPath, "app.asar.unpacked", "server", "index.js");
const serverProcess = spawn(process.execPath, [serverPath], {
stdio: "inherit",
});
// Start Python process
const pythonPath = path.join(process.resourcesPath, "app.asar.unpacked", "python", "myapp");
const pythonProcess = spawn(pythonPath, [], {
stdio: "inherit",
});
serverProcess.on("error", (err) => console.error("Server process error:", err));
pythonProcess.on("error", (err) => console.error("Python process error:", err));
};
app.whenReady().then(createWindow);
I’ve already done the following:
Configured package.json with the right build settings
Set up extraResources / asarUnpack to include the server and Python files
Verified both child processes work standalone
Questions:
What’s the correct way to package and spawn these child processes for macOS production builds?
Do I need to move them into a specific location (like Contents/Resources/app.asar.unpacked) and reference them differently?
Is there a more reliable pattern for handling Express + Python child processes inside an Electron app bundle?
Any insights or working examples would be really appreciated!
I'm working on a Mac app that receives a process ID via NSXPCConnection, and I'm trying to figure out the best way to determine whether that process is a native macOS app like Safari—with bundles and all—or just a script launched by something like Node or Python. The executable is signed with a Team ID using codesign.
I was thinking about getting the executable's path as one way to handle it, but I’m wondering if there’s a more reliable method than relying on the folder structure.
Topic:
App & System Services
SubTopic:
Processes & Concurrency
Tags:
XPC
Inter-process communication
Hello im creating an expo module using this new API, but the problem i found currently testing this functionality is that when the task fails, the notification error doesn't go away and is always showing the failed task notification even if i start a new task and complete that one.
I want to implement this module into the production app but i feel like having always the notification error might confuse our users or find it a bit bothersome.
Is there a way for the users to remove this notification?
Best regards!
Some users of my Mac app are complaining of redrawing delays. Based on what I see in logs, my GCD timer event handlers are not being run in a timely manner although the runloop is still pumping events: sometimes 500ms pass before a 15ms timer runs. During this time, many keypresses are routed through -[NSApplication sendEvent:], which is how I know it's not locked up in synchronous code.
This issue has not been reported in older versions of macOS.
I start the timer like this:
_gcdUpdateTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_gcdUpdateTimer,
dispatch_time(DISPATCH_TIME_NOW, period * NSEC_PER_SEC),
period * NSEC_PER_SEC,
0.0005 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_gcdUpdateTimer, ^{
…redraw…
});
I was stuck on a long train journey this weekend, so I thought I’d use that time to write up the process for installing a launchd daemon using SMAppService. This involves a number of deliberate steps and, while the overall process isn’t too hard — it’s certainly a lot better than with the older SMJobBless — it’s easy to accidentally stray from the path and get very confused.
If you have questions or comments, start a new thread in the App & System Services > Processes & Concurrency subtopic and tag it with Service Management.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Getting Started with SMAppService
This post explains how to use SMAppService to install a launchd daemon. I tested these instructions using Xcode 26.0 on macOS 15.6.1. Things are likely to be slightly different with different Xcode and macOS versions.
Create the container app target
To start, I created a new project:
I choose File > New > Project.
In the template picker, I chose macOS > App.
In options page, I set the Product Name field to SMAppServiceTest [1].
And I selected my team in the Team popup.
And I verified that the Organization Identifier was set to com.example.apple-samplecode, the standard for Apple sample code [1].
I selected SwiftUI in the Interface popup. There’s no requirement to use SwiftUI here; I chose it because that’s what I generally use these days.
And None in the Testing System popup.
And None in the Storage popup.
I then completed the new project workflow.
I configured basic settings on the project:
In the Project navigator, I selected the SMAppServiceTest project.
In the Project editor, I selected the SMAppServiceTest target.
At the top I selected Signing & Capabilities.
In the Signing section, I made sure that “Automatically manage signing” was checked.
And that my team was selected in the Team popup.
And that the bundle ID of the app ended up as com.example.apple-samplecode.SMAppServiceTest.
Still in the Signing & Capabilities tab, I removed the App Sandbox section.
Note It’s possible to use SMAppService to install a daemon from a sandboxed app, but in that case the daemon also has to be sandboxed. That complicates things, so I’m disabling the sandbox for the moment. See Enable App Sandbox, below, for more on this.
Next I tweaked some settings to make it easier to keep track of which target is which:
At the top, I selected the Build Settings tab.
I changed the Product Name build setting from $(TARGET_NAME) to SMAppServiceTest.
On the left, I renamed the target to App.
I chose Product > Scheme > Manage Schemes.
In the resulting sheet, I renamed the scheme from SMAppServiceTest to App, just to keep things in sync.
[1] You are free to choose your own value, of course. However, those values affect other values later in the process, so I’m giving the specific values I used so that you can see how everything lines up.
Create the daemon target
I then created a daemon target:
I chose File > New > Target.
In the template picker, I chose macOS > Command Line Tool.
In the options page, I set the Product Name field to Daemon.
And I selected my team in the Team popup.
And I verified that the Organization Identifier was set to com.example.apple-samplecode, the standard for Apple sample code.
I selected Swift in the Language popup.
And verified that SMAppServiceTest was set in the Project popup.
I clicked Finish.
I configured basic settings on the target:
In the Project navigator, I selected the SMAppServiceTest project.
In the Project editor, I selected the Daemon target.
At the top I selected Signing & Capabilities.
In the Signing section, I made sure that “Automatically manage signing” was checked.
And that my team was selected in the Team popup.
Note The Bundle Identifier field is blank, and that’s fine. There are cases where you want to give a daemon a bundle identifier, but it’s not necessary in this case.
Next I tweaked some settings to make it easier to keep track of which target is which:
At the top, I selected the Build Settings tab.
I changed the Product Name build setting from $(TARGET_NAME) to SMAppServiceTest-Daemon.
I forced the Enable Debug Dylib Support to No.
IMPORTANT To set it to No, you first have to set it to Yes and then set it back to No.
I edited Daemon/swift.swift to look like this:
import Foundation
import os.log
let log = Logger(subsystem: "com.example.apple-samplecode.SMAppServiceTest", category: "daemon")
func main() {
log.log("Hello Cruel World!")
dispatchMain()
}
main()
This just logs a ‘first light’ log message and parks [1] the main thread in dispatchMain().
Note For more about first light log points, see Debugging a Network Extension Provider.
[1] Technically the main thread terminates in this case, but I say “parks” because that’s easier to understand (-:
Test the daemon executable
I selected the Daemon scheme and chose Product > Run. The program ran, logging its first light log entry, and then started waiting indefinitely.
Note Weirdly, in some cases the first time I ran the program I couldn’t see its log output. I had to stop and re-run it. I’m not sure what that’s about.
I chose Product > Stop to stop it. I then switched back the App scheme.
Embed the daemon in the app
I added a build phase to embed the daemon executable into app:
In the Project navigator, I selected the SMAppServiceTest project.
In the Project editor, I selected the App target.
At the top I selected Build Phases.
I added a new copy files build phase.
I renamed it to Embed Helper Tools.
I set its Destination popup to Executables.
I clicked the add (+) button under the list and selected SMAppServiceTest-Daemon.
I made sure that Code Sign on Copy was checked for that.
I then created a launchd property list file for the daemon:
In the Project navigator, I selected SMAppServiceTestApp.swift.
I chose Product > New > File from Template.
I selected the Property List template.
In the save sheet, I named the file com.example.apple-samplecode.SMAppServiceTest-Daemon.plist.
And made sure that the Group popup was set to SMAppServiceTest.
And that only the App target was checked in the Targets list.
I clicked Create to create the file.
In the property list editor, I added two properties:
Label, with a string value of com.example.apple-samplecode.SMAppServiceTest-Daemon
BundleProgram, with a string value of Contents/MacOS/SMAppServiceTest-Daemon
I added a build phase to copy that property list into app:
In the Project navigator, I selected the SMAppServiceTest project.
In the Project editor, I selected the App target.
At the top I selected Build Phases.
I added a new copy files build phase.
I renamed it to Copy LaunchDaemons Property Lists.
I set its Destination popup to Wrapper.
And set the Subpath field to Contents/Library/LaunchDaemons.
I disclosed the contents of the Copy Bundle Resources build phase.
I dragged com.example.apple-samplecode.SMAppServiceTest-Daemon.plist from the Copy Bundle Resources build phase to the new Copy LaunchDaemons Property Lists build phase.
I made sure that Code Sign on Copy was unchecked.
Register and unregister the daemon
In the Project navigator, I selected ContentView.swift and added the following to the imports section:
import os.log
import ServiceManagement
I then added this global variable:
let log = Logger(subsystem: "com.example.apple-samplecode.SMAppServiceTest", category: "app")
Finally, I added this code to the VStack:
Button("Register") {
do {
log.log("will register")
let service = SMAppService.daemon(plistName: "com.example.apple-samplecode.SMAppServiceTest-Daemon.plist")
try service.register()
log.log("did register")
} catch let error as NSError {
log.log("did not register, \(error.domain, privacy: .public) / \(error.code)")
}
}
Button("Unregister") {
do {
log.log("will unregister")
let service = SMAppService.daemon(plistName: "com.example.apple-samplecode.SMAppServiceTest-Daemon.plist")
try service.unregister()
log.log("did unregister")
} catch let error as NSError {
log.log("did not unregister, \(error.domain, privacy: .public) / \(error.code)")
}
}
IMPORTANT None of this is code is structured as I would structure a real app. Rather, this is the absolutely minimal code needed to demonstrate this API.
Check the app structure
I chose Product > Build and verified that everything built OK. I then verified that the app’s was structured correctly:
I then choose Product > Show Build Folder in Finder.
I opened a Terminal window for that folder.
In Terminal, I changed into the Products/Debug directory and dumped the structure of the app:
% cd "Products/Debug"
% find "SMAppServiceTest.app"
SMAppServiceTest.app
SMAppServiceTest.app/Contents
SMAppServiceTest.app/Contents/_CodeSignature
SMAppServiceTest.app/Contents/_CodeSignature/CodeResources
SMAppServiceTest.app/Contents/MacOS
SMAppServiceTest.app/Contents/MacOS/SMAppServiceTest.debug.dylib
SMAppServiceTest.app/Contents/MacOS/SMAppServiceTest
SMAppServiceTest.app/Contents/MacOS/__preview.dylib
SMAppServiceTest.app/Contents/MacOS/SMAppServiceTest-Daemon
SMAppServiceTest.app/Contents/Resources
SMAppServiceTest.app/Contents/Library
SMAppServiceTest.app/Contents/Library/LaunchDaemons
SMAppServiceTest.app/Contents/Library/LaunchDaemons/com.example.apple-samplecode.SMAppServiceTest-Daemon.plist
SMAppServiceTest.app/Contents/Info.plist
SMAppServiceTest.app/Contents/PkgInfo
There are a few things to note here:
The com.example.apple-samplecode.SMAppServiceTest-Daemon.plist property list is in Contents/Library/LaunchDaemons.
The daemon executable is at Contents/MacOS/SMAppServiceTest-Daemon.
The app is still built as debug dynamic library (SMAppServiceTest.debug.dylib) but the daemon is not.
Test registration
I chose Product > Run. In the app I clicked the Register button. The program logged:
will register
did not register, SMAppServiceErrorDomain / 1
Error 1 indicates that installing a daemon hasn’t been approved by the user. The system also presented a notification:
Background Items Added
“SMAppServiceTest” added items that can
run in the background for all users. Do you
want to allow this?
Options > Allow
> Don’t Allow
I chose Allow and authenticated the configuration change.
In Terminal, I verified that the launchd daemon was loaded:
% sudo launchctl list com.example.apple-samplecode.SMAppServiceTest-Daemon
{
"LimitLoadToSessionType" = "System";
"Label" = "com.example.apple-samplecode.SMAppServiceTest-Daemon";
"OnDemand" = true;
"LastExitStatus" = 0;
"Program" = "Contents/MacOS/SMAppServiceTest-Daemon";
};
IMPORTANT Use sudo to target the global launchd context. If you omit this you end up targeting the launchd context in which Terminal is running, a GUI login context, and you won't find any launchd daemons there.
I started monitoring the system log:
I launched the Console app.
I pasted subsystem:com.example.apple-samplecode.SMAppServiceTest into the search box.
I clicked “Start streaming”.
Back in Terminal, I started the daemon:
% sudo launchctl start com.example.apple-samplecode.SMAppServiceTest-Daemon
In Console, I saw it log its first light log point:
type: default
time: 17:42:20.626447+0100
process: SMAppServiceTest-Daemon
subsystem: com.example.apple-samplecode.SMAppServiceTest
category: daemon
message: Hello Cruel World!
Note I’m starting the daemon manually because my goal here is to show how to use SMAppService, not how to use XPC to talk to a daemon. For general advice about XPC, see XPC Resources.
Clean up
Back in the app, I clicked Unregister. The program logged:
will unregister
did unregister
In Terminal, I confirmed that the launchd daemon was unloaded:
% sudo launchctl list com.example.apple-samplecode.SMAppServiceTest-Daemon
Could not find service "com.example.apple-samplecode.SMAppServiceTest-Daemon" in domain for system
Note This doesn’t clean up completely. The system remembers your response to the Background Items Added notification, so the next time you run the app and register your daemon it will be immediately available. To reset that state, run the sfltool with the resetbtm subcommand.
Install an Agent Rather Than a Daemon
The above process shows how to install a launchd daemon. Tweaking this to install a launchd agent is easy. There are only two required changes:
In the Copy Launch Daemon Plists copy files build phase, set the Subpath field to Contents/Library/LaunchAgents.
In ContentView.swift, change the two SMAppService.daemon(plistName:) calls to SMAppService.agent(plistName:).
There are a bunch of other changes you should make, like renaming everything from daemon to agent, but those aren’t required to get your agent working.
Enable App Sandbox
In some cases you might want to sandbox the launchd job (the term job to refer to either a daemon or an agent.) This most commonly crops up with App Store apps, where the app itself must be sandboxed. If the app wants to install a launchd agent, that agent must also be sandboxed. However, there are actually four combinations, of which three are supported:
App Sandboxed | Job Sandboxed | Supported
------------- | ------------- | ---------
no | no | yes
no | yes | yes
yes | no | no [1]
yes | yes | yes
There are also two ways to sandbox the job:
Continue to use a macOS > Command Line Tool target for the launchd job.
Use an macOS > App target for the launchd job.
In the first approach you have to use some low-level build settings to enable the App Sandbox. Specifically, you must assign the program a bundle ID and then embed an Info.plist into the executable via the Create Info.plist Section in Binary build setting.
In the second approach you can use the standard Signing & Capabilities editor to give the job a bundle ID and enable the App Sandbox, but you have to adjust the BundleProgram property to account for the app-like wrapper.
IMPORTANT The second approach is required if your launchd job uses restricted entitlements, that is, entitlements that must be authorised by a provisioning profile. In that case you need an app-like wrapper to give you a place to store the provisioning profile. For more on this idea, see Signing a daemon with a restricted entitlement.
For more background on how provisioning profiles authorise the use of entitlements, see TN3125 Inside Code Signing: Provisioning Profiles.
On balance, the second approach is the probably the best option for most developers.
[1] When SMAppService was introduced it was possible to install a non-sandboxed daemon from a sandboxed app. That option is blocked by macOS 14.2 and later.