Inspired by an article on nilcoalescing.com, I wanted to finally add two services to Leviathan, the collection & deck management app for Mac (and iOS, but services won’t work there).
- Import deck list
This service should take a text selection and treat it as a structured deck list. Importing cards from a text list is already supported as a feature inside the app, so I only needed the service to transfer a string to this existing part of the app. - Look up card
This service should take a text selection and treat it as a search term for a single magic card. Again, the feature is already in the app and supports complex Scryfall search terms as well as simple card names.

The article mentioned above explains the basics very well, but:
- Leviathan is a SwiftUI app, not AppKit
- Both features don’t just convert the input into a new output, but need to launch the app to show UI for completing the task.
I found that the best way to make the service launch the app is to register an URL scheme for it that can be called in the service method. I added this definition (to Info.plist) for both the iOS and the macOS target, since this not dependent on services per se and might be useful either way:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>leviathan</string>
<key>CFBundleURLSchemes</key>
<array>
<string>leviathan</string>
</array>
</dict>
</array>
Make the SwiftUI app handle the URL with the handlesExternalEvents modifier on your WindowGroup and the openURL modifier on your window:
@main
struct LeviathanApp: App {
var body: some Scene {
WindowGroup {
LeviathanWindow()
.handlesExternalEvents(matching: ["importCards", "lookupCard"])
}
}
}
struct LeviathanWindow: View {
var body: some View {
NavigationContainer()
.onOpenURL(perform: { url in
if url.host() == "lookupCard" {
// Display/call the import card feature
} else if url.host() == "importCards" {
// Display/call the card lookup feature
}
}
}
}
Then the service provider class can call the URL scheme with NSWorkspace.shared.open(url). This file targets macOS only:
import AppKit
@MainActor
class ServiceProvider: NSObject {
@objc func importCards(_ pasteboard: NSPasteboard, userData: String?, error:
AutoreleasingUnsafeMutablePointer<NSString>) {
let input = pasteboard.string(forType: NSPasteboard.PasteboardType.string)
let inputQueryValue = input?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
var path = "leviathan://importCards"
if let inputQueryValue {
path.append("?input=\(inputQueryValue)")
}
let url = URL(string: path)!
NSWorkspace.shared.open(url)
}
@objc func lookupCard(_ pasteboard: NSPasteboard, userData: String?, error:
AutoreleasingUnsafeMutablePointer<NSString>) {
let input = pasteboard.string(forType: NSPasteboard.PasteboardType.string)
let inputQueryValue = input?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
var path = "leviathan://lookupCard"
if let inputQueryValue {
path.append("?input=\(inputQueryValue)")
}
let url = URL(string: path)!
NSWorkspace.shared.open(url)
}
}
Now you need to register the ServiceProvider class. I think the best way is to create an AppKit AppDelegate (despite modern SwiftUI architecture not needing one). Again, this file is only added to the macOS target:
import AppKit
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.servicesProvider = ServiceProvider()
}
}
And you need to adapt LeviathanApp (from above) a bit to register the app delegate:
@main
struct LeviathanApp: App {
#if os(macOS)
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#endif
var body: some Scene {
WindowGroup {
LeviathanWindow()
.handlesExternalEvents(matching: ["importCards", "lookupCard"])
}
}
}
That’s it, the rest is more or less described in the Provide macOS system-wide services from your app article. Make sure to log out and back in to your mac account after compiling and launching your app to see changes in the services menu.
If you find a better way to reliably open your SwiftUI app window by calling a macOS system service, let me know!