swiftobjective-cmacosxpcnsxpcconnection

How to whitelist XPC custom classes in Swift?


I have a pair of Swift Xcode targets that implement the XPC main app + service app pattern. I figured out how to pass custom classes back and forth, either as arguments to the remote object's methods or "return values" (i.e., arguments the completion handler/reply block). This all works well.

Now I am trying to add a new wrinkle: call the completion handler with an NSArray of my custom classes from the service to return them to the main app. I understand from Apple's docs (and this Obj-C answer) that I need to whitelist my custom class on the receiving end's NSXPCInterface. But I can't figure out how to do this in Swift. All the examples online and in docs are in Obj-C and I'm having trouble finding the correct equivalents in Swift.

Concretely, here's a simplified version of my custom classes:

@objc(PartialSnapshot) public class PartialSnapshot : NSObject, NSSecureCoding {
    public var rawPaths: [String]

    public static var supportsSecureCoding: Bool = true
    
    public func encode(with coder: NSCoder) {
        coder.encode(rawPaths, forKey: "rawPaths")
    }

    public required init?(coder: NSCoder) {
        guard
            let rawPaths = coder.decodeObject(of: [NSArray.self], forKey: "rawPaths") as? [String]
        else {
            NSLog("PartialSnapshot: returning nil from init?(coder: NSCoder)")
            return nil
        }
        self.rawPaths = rawPaths
    } 
}

@objc(ReadAllSnapshotsRpcReturnType) public class ReadAllSnapshotsRpcReturnType : NSObject, NSSecureCoding {
    var partials: [PartialSnapshot]

    public static var supportsSecureCoding: Bool = true

    public func encode(with coder: NSCoder) {
        coder.encode(partials, forKey: "partials")
    }

    public required init?(coder: NSCoder) {
        guard
            let partials = coder.decodeObject(of: [NSArray.self], forKey: "partials") as? [PartialSnapshot]
        else {
            NSLog("ReadAllSnapshotsRpcReturnType: returning nil from init?()")
            return nil
        }
        self.partials = partials
    }
}

And my method signature on the remote object's protocol:

@objc func ReadAllSnapshotsRpc(then completion: @escaping (ReadAllSnapshotsRpcReturnType?, Error?) -> Void)

I think I know roughly what I need to do from this Objective-C answer. When I create my NSXPCConnection in the main app, I need to add a line like the third one below:

let connection = NSXPCConnection(machServiceName: SecurityProxyConstants.domain, options: .privileged)
connection.remoteObjectInterface = NSXPCInterface(with: SecurityProxyProtocol.self)
connection.remoteObjectInterface?.setClasses(ReadAllSnapshotsRpcReturnType.self, for: #selector(PartialSnapshot.self), argumentIndex: 0, ofReply: true)

However, I clearly don't have the syntax quite right and the many syntactical variations I have tried also aren't compiling. Can anyone point me in the right direction?

EDIT: This is the runtime error I am getting that leads me to believe I need to whitelist:

<NSXPCConnection: 0x600003f5b0c0> connection to service with pid 9679 named com.mycompany.MyApp.SecurityProxy: Exception caught during decoding of reply to message 'ReadAllSnapshotsRpcWithThen:', dropping incoming message and calling failure block.

Ignored Exception: Exception while decoding argument 0 (#1 of invocation):
<NSInvocation: 0x600001bdb740>
return value: {v} void
target: {@?} 0x0 (block)
argument 1: {@} 0x0
argument 2: {@} 0x0

Exception: value for key 'NS.objects' was of unexpected class 'PartialSnapshot' (0x10c7abe00) [/Users/mwg/Library/Developer/Xcode/DerivedData/MyApp-brumjpaxvoxfmjchpswlfnbsarho/Build/Products/Debug/MyApp.app].
Allowed classes are:
 {(
    "'NSArray' (0x7ff8485798a0) [/System/Library/Frameworks/CoreFoundation.framework]"
)}

Solution

  • You don't need whitelist in this case, here is by doc:

    doc

    it would be needed if your published interface looks like

    @objc func ReadAllSnapshotsRpc(then completion: @escaping (NSArray?, Error?) -> Void)
    

    i.e. if you would return an opaque array of unknown what, but you return explicitly typed object, so everything else is on NSSecureCoding as stated.

    Update: the reason as error stated is in decoding - for decoding all class should be enumerated. Below is fixed variant:

    public required init?(coder: NSCoder) {
        guard
            let partials = coder.decodeObject(of: [NSArray.self, PartialSnapshot.self], forKey: "partials") as? [PartialSnapshot]
        else {
            NSLog("ReadAllSnapshotsRpcReturnType: returning nil from init?()")
            return nil
        }
        self.partials = partials
    }