Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-05-07

Adding a System Service to operate on files

On occasion you may want to add a system service to your application. For example when your application can process files. As is the case for the app I am currently working on. The service can then appear in the Service menu that is a.o. available in the finder on a right-click.

For my application this simply means that I want the user to be able to select a number of files, and then right-click on those files and select "Open in MyGreatApp". Upon which MyGreatApp will launch (if necessary) and open/process the selected files.

In the above case, the OS will collect the paths of the selected files in an array and put that array on a pasteboard. It will then call a specific function in MyGreatApp (on a specific object) and hand over the pasteboard to that function. The function to be called is specified in the into.plist of MyGreatApp.

There are of course many variations possible, so I will only document here what was necessary in my case. For more information see: Service Implementation Guide.

First things first: in my app I decided to put the service call function in the AppDelegate.

class AppDelegate: NSObject, NSApplicationDelegate {

    // Other stuff ...

    func applicationDidFinishLaunching(aNotification: NSNotification) {
      
        // Other stuff ...

        NSApplication.sharedApplication().servicesProvider = self
        
    }
    
    func myService(pboard: NSPasteboard, userData: String, error: AutoreleasingUnsafeMutablePointer<NSString?>) {
        
        if impossible() {
            error.memory = "Failure, application did not start properly"
            return
        }
        
        if let files = pboard.propertyListForType(NSFilenamesPboardType) as? Array<String> {
            dataSource.insertLines(files, atIndex: 0)
        }
    }
}

You can see the two necessary things, I have added:

NSApplication.sharedApplication().servicesProvider = self

this is necessary to let the OS know which object will handle the service.

Next is:

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

this is the service that will be called by the OS when the user selects the proper menu entry.

How does the OS know which function implements the service? simple, we say so in the info.plist:

<key>NSServices</key>
<array>
<dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>Open in MyGreatApp</string>
</dict>
<key>NSMessage</key>
<string>myService</string>
<key>NSPortName</key>
<string>MyGreatApp</string>
<key>NSSendFileTypes</key>
<array>
<string>public.item</string>
</array>
<key>NSServiceDescription</key>
<string>Describe what the service will do</string>
<key>NSTimeout</key>
<string>2000</string>
</dict>

</array>

You can edit this directly into the plist, or you can select your target and choose the "info" tab entry. There -at the bottom- you'll see the "Service (0)" item which you can fill in with the necessary data. The later method is a bit of a hassle when you need "Additional service properties" because not all available properties are defined in the combibox selector. I.e. you need to specify them manually using the names as defined in the Service Implementation Guide.

Also, do not use both methods, choose either to edit the plist directly, or the target-info tab. Using both of them at the same time will cause problems because Xcode does not seem to handle concurrent editing of the info.plist very well.

Notice that I used the "public.item" for the NSSendFileTypes. This was a bit of try-and-error on my part as there is no universal identifier specifically to designate any file. (public.file-url does not work since it will place the service under 'internet' in the service menu)

Do read the Service Implementation Guide as I did not use all available options above, and your milage will most likely be different.

OK, now we have the service defined and implement, lets try if it works...

Uh, not so fast. Yes you can build and run your app, but the service won't be found. The app needs to be in the applications folder, otherwise the OS will not scan the info.plist. Thus you will need to copy the app from the build folder and put it in the "Applications" folder.

So now...?

No, not yet. Since the application folder is only scanned (for the info.plist) at login, we need to force a rescan by hand. Open up a terminal window and type:

/System/Library/CoreServices/pbs -update

You can even check if the service was correctly read by executing the following command:

/System/Library/CoreServices/pbs -dump_pboard

See your service? fine then now... nope. Not yet.
You still need to update the service preferences to enable your service.
Select the menu entry: "Xcode -> Services -> Services Preferences..." (The application you do this in does not matter, Finder works fine too). Search for your service, and tick the box in front of it. Oh, btw: your user will also need to enable the service or he/she won't be able to use it!)

Now, finally go ahead and test your service.

Chances are, it won't work perfectly the first time, and may even crash. Unfortunately you will not see anything in the console because the app runs on its own, outside Xcode. Fortunately this is easily remedied: Now that the OS knows about the service and the app, you can start the app from Xcode, and then switch to the Finder to select a couple of files and right-click & select your service. The OS now knows that your app is already running and it will use that instead of starting a new one. From here on you will get error messages in the Xcode console, and you can debug as you are used to.

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

2 comments:

  1. As Services Implementation Guide says, use of /System/Library/CoreServices/pbs is discouraged as it may be unavailable in forthcoming releases. It should be used for debugging purposes only.

    I think I never copied one of my Apps to the Applications folder but still had them in Services Menu.

    It is not explicitly documented but I found via testing and today read confirming posts that NSRequiredContext (its value may be empty) needs to be added to services entries in plist to make them active by default.
    I think requiring users to open SystemPreferences to find a certain service in a long list just to activate it or to change shortcuts is not good. I am still looking for a solution to the latter problem which does not use /System/Library/CoreServices/pbs.

    ReplyDelete
  2. Thanks for your comments Marco,

    In case it is unclear, let me add here that the usage of 'pbs' in the post was indeed intended for the developper only. We should never burden a user with that.

    I had to copy my app to the applications folder. It now occurs to me that this may depend on whether or not the App was available at boot-time or not. The 'pbs' utility probably scans only the Applications directory (??). Anyway I have not tested this hypothesis.

    I do not use a NSRequiredContext, and the service works fine. Maybe this is a recent development? Anyway, good to know in case somebody has problems. Thanks.

    Agree on the SystemPreferences, that sucks. If you find a solution, I am sure a lot of people would like to see it mentioned here ;-)

    ReplyDelete