Search code examples
iosxcodexpcxcode-extension

How do I allow my Xcode Source Editor Extension to use XPC?


In this article on Xcode Source Editor extensions, it mentions that XPC is a way to circumvent the app sandbox:

The extension must be sandboxed just to be loaded by Xcode, whereas calls to SourceKit needs to be un-sandboxed, which of course won’t fly in the App Store. We could distribute independently and use an un-sandboxed XPC service embedded in the extension.

However, I'm not sure how to tie everything together to use an XPC service.

How do I tie my Xcode Source Editor extension to an XPC service?


Solution

  • I was able to figure this out thanks to the LinuxSupportForXcode extension.

    I'm going to make the assumption that you followed the tutorial on creating an Xcode Extension Editor, and made the main project a macOS App. You should have a target structure similar to:

    • MyApp (macOS App target)
    • MyAppExtension (Xcode Source Editor Extension target)

    To use XPC with a Source Editor Extension:

    1. File > New > Target... > XPC Service.

      For example purposes, we'll assume it's called MyAppXPCService and its bundle identifier is com.example.MyAppXPCService.

    2. Move the XPC service dependency from the App to the Extension:

      If you don't do this step, you may run into issues where your XPCService isn't being executed by the extension. E.g. you invoke a command that should launch the XPCService, but in the Xcode Debug Navigator, your XPCService never comes up.

      1. Go to your app's target.
      2. Remove MyAppXPCService.xpc from the Frameworks and Libraries.
      3. Go to your extension's target.
      4. Add MyAppXPCService.xpc to Frameworks and Libraries by dragging it in from the products folder in the Project Navigator. Leave it on the default "Embed Without Signing".
    3. In the XPC Service, convert it to Swift, mainly following the instructions here:

      Note: If you'd prefer to not convert to Swift, and use a mixed target instead, simply create a Swift file and when prompted, choose to create the bridging header, then include #import "MyAppXPCServiceProtocol.h" in the bridging header.

      1. Create main.swift, MyService.swift, MyServiceDelegate.swift, MyServiceProtocol.swift normally.

      2. Set the following build settings:

        • Install Objective-C Compatibility Header: NO
        • Objective-C Generated Interface Header Name: `` (blank)
      3. Choose your desired Swift Language Version in Build Settings.

      4. In Build Settings, add (don't replace): @loader_path/../../../../Frameworks to Runtime Search Paths.

        If you accidentally replace, and use an embedded framework, XPC will crash on launch.

    4. In your extension target:

      1. import MyAppXPCService so that it can see the protocol.

      2. Create the connection, using your XPC target's bundle identifier for serviceName:

        private let connection = { () -> NSXPCConnection in
          let connection = NSXPCConnection(serviceName: <#"com.example.MyAppXPCService"#>)
          connection.remoteObjectInterface = NSXPCInterface(with: MyAppXPCServiceProtocol.self)
          return connection
        }()
        
      3. Call your XPC service:

        private func xpcUpperCase(input: String) {
          connection.resume()
          defer { connection.suspend() }
          guard let xpcService = connection.remoteObjectProxy as? MyAppXPCServiceProtocol else {
            throw NSError()
          }
        
          xpcService.upperCaseString(input) { output in
            print(output)
          }
        }
        

    The nice thing about creating the connection is it'll automatically start your XPC service, without needing the UI app to be running.