Provide macOS system-wide services from your app
Services on macOS allow us to extend our app’s functionality to the entire system, enabling users to interact with our app’s features while working in other contexts without explicitly opening it. These services are accessible via the context menu or from an application's Services menu in the macOS menu bar.
I recently implemented services for URL encoding and decoding strings in my macOS utility, EncodeDecode, and in this post, I’ll explain how I did it.
![macOS context menu with 'URL-Decode' and 'URL-Encode' services for text selection](/static/blog/macOSSystemWideServices/TextEdit.ae15LqsGzRkd4pk4FBr5b5gtU0sp4sNEbfxXXdG-DUs.png)
For my app, I implemented two services: URL-Encode and URL-Decode. These services become available when users select text anywhere on the system, such as in TextEdit or Xcode. When a user selects text and invokes one of the services, macOS launches EncodeDecode, passes the selected string to my app for processing, and replaces the original text with the processed result.
# Define service methods in code
To implement this functionality, I needed to define the required methods in code. This involved creating a service provider class that inherits from NSObject
and implementing methods that process the text from the pasteboard.
import AppKit
class EncodeDecodeServiceProvider: NSObject {
@objc func encode(
_ pasteboard: NSPasteboard,
userData: String?,
error: AutoreleasingUnsafeMutablePointer<NSString>
) {
guard let string = pasteboard.string(
forType: NSPasteboard.PasteboardType.string
) else {
return
}
pasteboard.clearContents()
pasteboard.setString(
EncodingManager.encodeUrl(string),
forType: .string
)
}
@objc func decode(
_ pasteboard: NSPasteboard,
userData: String?,
error: AutoreleasingUnsafeMutablePointer<NSString>
) {
guard let string = pasteboard.string(
forType: NSPasteboard.PasteboardType.string
) else {
return
}
pasteboard.clearContents()
pasteboard.setString(
EncodingManager.decode(urlString: string),
forType: .string
)
}
}
# Register the service provider
After defining the service methods, the next step was to register the service provider. I did it in the applicationDidFinishLaunching(_:)
method of my application delegate.
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(
_ aNotification: Notification
) {
NSApplication.shared
.servicesProvider = EncodeDecodeServiceProvider()
}
}
Only one service provider can be registered per application. If our app provides multiple services, a single object must handle all of them, as I did in EncodeDecode.
# Update Info.plist
Finally, I needed to declare the services in the Info.plist
file. Here is how the NSServices key is configured for EncodeDecode:
<key>NSServices</key>
<array>
<dict>
<key>NSKeyEquivalent</key>
<dict>
<key>default</key>
<string>E</string>
</dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>URL-Encode</string>
</dict>
<key>NSMessage</key>
<string>encode</string>
<key>NSPortName</key>
<string>EncodeDecode</string>
<key>NSRestricted</key>
<false/>
<key>NSRequiredContext</key>
<dict/>
<key>NSReturnTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
<key>NSSendTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
</dict>
<dict>
<key>NSKeyEquivalent</key>
<dict>
<key>default</key>
<string>D</string>
</dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>URL-Decode</string>
</dict>
<key>NSMessage</key>
<string>decode</string>
<key>NSPortName</key>
<string>EncodeDecode</string>
<key>NSRestricted</key>
<false/>
<key>NSRequiredContext</key>
<dict/>
<key>NSReturnTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
<key>NSSendTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
</dict>
</array>
The NSServices
key contains an array of dictionaries, each defining a service. For EncodeDecode, there are two services: URL-Encode
and URL-Decode
. Each service specifies the name as it appears in the Services menu, along with a keyboard shortcut for quick access. The NSMessage
key maps to the selector method handling the service, and NSPortName
identifies the app providing the service. Both services define NSSendTypes
and NSReturnTypes
to specify that they work with text data from the pasteboard.
To simplify the process of adding and editing these configurations, I used the XML editor in Xcode. You can access it by right-clicking on the Info.plist
file and selecting Open As > Source Code. This approach provides full control and makes it easier to define the necessary keys and values.
# Testing and debugging
During development and testing, I found that the most reliable way to see updated services was to log out of my macOS user account and log back in.
To ensure that the system has recognized your service, you can also use the pbs
tool to list the registered services. Run the following command in the terminal with the dump_pboard
option:
/System/Library/CoreServices/pbs -dump_pboard
This command will display a list of registered services, allowing you to verify that your service has been correctly registered.
Users can manage available services in System Settings under Keyboard > Keyboard Shortcuts > Services. They can enable or disable services, assign shortcuts for quick access, and ensure only relevant services appear in an app’s Services menu.
![System Settings showing the Services section under Keyboard Shortcuts, with options like URL-Encode and URL-Decode enabled](/static/blog/macOSSystemWideServices/SystemSettings.X2Aiad_uYkpBhycHLXthWtxpeaCOatESW02ctn9kHh8.png)
If you're an experienced Swift developer looking to learn advanced techniques, check out my latest book Swift Gems. It’s packed with tips and tricks focused solely on the Swift language and Swift Standard Library. From optimizing collections and handling strings to mastering asynchronous programming and debugging, "Swift Gems" provides practical advice that will elevate your Swift development skills to the next level. Grab your copy and let's explore these advanced techniques together.