Search code examples
swiftservicecontextmenufinder

Establish a service in Finder context


I'm trying to add a service to the Finder's context menu using this class:

public class Service {
  public func handleServices(pboard:NSPasteboard, userData:String, error:UnsafeMutableBufferPointer<String>) {  // not sure about the correct parameters
    if (pboard.types?.contains(NSFilenamesPboardType) != nil) {
      let fileArray = pboard.propertyListForType(NSFilenamesPboardType)
      print(fileArray)
    }
  }

  init () {
    NSApp.servicesProvider = self
    NSUpdateDynamicServices()
  }
}

The service is announced in info.plist as follows:

<key>NSServices</key>
<array>
    <dict>
        <key>NSMenuItem</key>
        <dict>
            <key>default</key>
            <string>Service Handling Demo</string>
        </dict>
        <key>NSMessage</key>
        <string>handleServices</string>
        <key>NSPortName</key>
        <string>services</string>
        <key>NSSendTypes</key>
        <array>
            <string>NSFilenamesPboardType</string>
        </array>
    </dict>
</array>

Finally I have turned on the service in System Preferences/Keyboard/Shortcuts. So I see the service and can call it. But all I get when calling it is

Cannot find service provider for selector handleServices:userData:error: or handleServices:: for service handleServices


Solution

  • There are two problems in your code:

    • Objective-C messages are sent to the service provider, therefore the Swift method must be "Objective-C compatible". This can be achieved by subclassing NSObject, or by marking the method with the @objc attribute.

    • The service handler method has the signature

      - (void)handleServices:(NSPasteboard *)pboard
                    userData:(NSString *)userData
                       error:(NSString **)error
      

      which is mapped to Swift as

      func handleServices(pboard: NSPasteboard!,
                        userData: String!,
                           error: AutoreleasingUnsafeMutablePointer<NSString?>)
      

    So this would be a correct version (which worked in my test):

    public class Service {
    
        @objc public func handleServices(pboard: NSPasteboard!,
            userData: String!, error: AutoreleasingUnsafeMutablePointer<NSString?>) {
    
            // ...
        }
    
        init() {
            NSApp.servicesProvider = self
            NSUpdateDynamicServices()
        }
    }
    

    Some more remarks:

    if (pboard.types?.contains(NSFilenamesPboardType) != nil) { ... }
    

    is "optional chaining" and checks if the contains() method could be called on pboard.types, in other words it checks only if pboard.types != nil. What you probably want is to check if pboard.types != nil and the contains() method returns true. This can be achieved with the "nil-coalescing operator" ??:

    if (pboard.types?.contains(NSFilenamesPboardType) ?? false) { ... }
    

    Next,

    pboard.propertyListForType(NSFilenamesPboardType)
    

    is documented to return an optional array of NSStrings, so you could unwrap and convert that to a String array with

    if let fileArray = pboard.propertyListForType(NSFilenamesPboardType) as? [String] { ... }
    

    Finally, assigning an error string (to the pointer provided by the caller) would be done with

    if (error != nil) {
        error.memory = "My error description"
    }