I noticed a problem while writing a program using XPC on macOS.
When I write it in the form of a closure that receives the result of an XPC call, I can't receive it forever.
I add an XPC target in Xcode, the sample code is used in the pass closure format, but can't I use closure passing with XPC?
My Environment:
- Xcode 15.3
- macOS 14.4.1
caller (closure version)
struct ContentView: View {
@State var callbackResult: String = "Waiting…"
var body: some View {
Form {
Section("Run XPC Call with no argument and no return value using callback") {
Button("Run…") {
callbackResult = "Running…"
let service = NSXPCConnection(serviceName: "net.mtgto.example-nsxpc-throws-error.ExampleXpc")
service.remoteObjectInterface = NSXPCInterface(with: ExampleXpcProtocol.self)
service.activate()
guard let proxy = service.remoteObjectProxy as? any ExampleXpcProtocol else { return }
defer {
service.invalidate()
}
proxy.performCallback {
callbackResult = "Done"
}
}
Text(callbackResult)
...
}
}
}
callee (closure version)
@objc protocol ExampleXpcProtocol {
func performCallback(with reply: @escaping () -> Void)
}
class ExampleXpc: NSObject, ExampleXpcProtocol {
@objc func performCallback(with reply: @escaping () -> Void) {
reply()
}
}
I found this problem can be solved by receiving asynchronous using Swift Concurrency.
caller (async version)
struct ContentView: View {
@State var callbackResult: String = "Waiting…"
var body: some View {
Form {
Section("Run XPC Call with no argument and no return value using callback") {
Button("Run…") {
simpleAsyncResult = "Running…"
Task {
let service = NSXPCConnection(serviceName: "net.mtgto.example-nsxpc-throws-error.ExampleXpc")
service.remoteObjectInterface = NSXPCInterface(with: ExampleXpcProtocol.self)
service.activate()
guard let proxy = service.remoteObjectProxy as? any ExampleXpcProtocol else { return }
defer {
service.invalidate()
}
await proxy.performNothingAsync()
simpleAsyncResult = "DONE"
}
Text(simpleAsyncResult)
...
}
}
}
callee (async version)
@objc protocol ExampleXpcProtocol {
func performNothingAsync() async
}
class ExampleXpc: NSObject, ExampleXpcProtocol {
@objc func performNothingAsync() async {}
}
To simplify matters, I write source code that omits the arguments and return value, but it is not also invoked by using callback style.
All sample codes are available in https://github.com/mtgto/example-nsxpc-throws-error
I’m not 100% sure what the actual problem is here. However:
-
Swift concurrency and
NSXPCConnection
aren’t a great combo. It’s better to usingNSXPCConnection
with traditional completion handlers. -
You’re not setting up any invalidation and interrupt handlers, which is problematic.
-
I generally recommend that you use
remoteObjectProxyWithErrorHandler(_:)
, so that you can receive error callbacks on a per-request basis. -
The
invalidate()
call in this sequence is problematic:
let service = NSXPCConnection(serviceName: "net.mtgto.example-nsxpc-throws-error.ExampleXpc")
service.remoteObjectInterface = NSXPCInterface(with: ExampleXpcProtocol.self)
service.activate()
guard let proxy = service.remoteObjectProxy as? any ExampleXpcProtocol else { return }
defer {
service.invalidate()
}
proxy.performCallback {
callbackResult = "Done"
}
You’re invalidating the connection at the end of the current scope, which is before the callback has any chance of being called.
In general, XPC connections are meant to be long-lived. Invalidating them after each request is considered poor form.
Finally, if you haven’t already read TN3113 Testing and debugging XPC code with an anonymous listener, you should. It’s the best way to get started with this stuff.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"