How to Call macOS APIs with Swift in Automator Workflows

I have an older Mac that I keep running on macOS Mojave because it’s the only way I’ll be able to run 32-bit applications. It supports iCloud Drive but doesn’t come with the menu option to remove downloads. That’s kind of important when its internal storage is so small.

I wanted to create my own menu item to call the Foundation API to handle this, but I didn’t want to build out a big Xcode project to do it. I’ve relied on Automator in the past, and I wondered: How far could it take me now?

After a little bit of digging, I discovered the API I needed: evictUbiquitousItem. But what I didn’t know was how to call it.

It seemed there might be a way I could use AppleScript to call this API. The thing is, I missed the AppleScript train by a lot. I trained myself on Linux and am much more comfortable in the Unix world. It also seemed like Apple wasn’t so keen on keeping AppleScript running, either.

There were also some references here and there to using the PyObjC framework. This worked on Mojave but didn’t look like it survived the transition to Python 3.

What I could get something done in, with a minimum of fuss and no Xcode, was Swift.

A Swift Script

Swift isn’t just good for building large applications in macOS. It also has a REPL and can be used to write tiny executable scripts.

For example:

#!/usr/bin/swift

print("Hello, world!")

Put this in a file and make it executable with chmod +x, and it’ll just run, after a bit of a compile delay the first time.

We can use this to build ourselves a script that’s very Unix-y, relying on the shell to parse arguments and do filename globbing so we don’t have to sweat it ourselves.

That makes dealing with filenames with special characters easy:

$ evict "Tax Returns" Pictures

Or, using wildcards:

$ evict *.mp4

The script looks like this:

#!/user/bin/env swift

import Foundation

for filename in CommandLine.arguments.dropFirst() {
    let url = URL.init(fileURLWithPath:filename)
    do {
        try FileManager.default.evictUbiquitousItem(at:url)
    } catch let error as NSError {
        print("\(filename): \(error.userInfo)")
    }
}

The shell feeds us a list of strings that are the filenames we want to work with. (The first item in the list is actually the program itself, by C convention, so we drop it.)

We iterate over this list, creating a URL from each filename for the API we want to call. We then call the API and catching and printing any errors.

So, now we can do this from the command line. Let’s make it a right-click option in Finder.

Enter Automator

Automator is a tool designed to let you build workflows in macOS and, as the name suggests, automate tasks.

One of the ways you can use Automator workflows is as Quick Actions, which lets us, among other places, put them in context menus in Finder.

The basic setup for our Quick Action Workflow looks like this:

Quick Action Workflow header illustration
Quick Action Workflow header

The important part of this is setting “Workflow receives current” to “files or folders”. This sets our workflow up to be able to receive a list of file paths that our script will then be able to process.

Now, we only have one thing we want Automator to do for us — run this script. There’s “Run AppleScript” and even “Run JavaScript,” but no “Run Swift.” But, there is a “Run Shell Script.” And we did, of course, create a script using Swift more or less as a shell.

The next problem we run into, though, is that there’s a fixed list of shells available. And despite having Python interpreters in the list, Swift doesn’t make an appearance.

Fear not, though — the venerable Bourne Shell, /bin/sh, is really useful.

/usr/bin/env swift - "$@" <<SWIFT

import Foundation

for filename in CommandLine.arguments.dropFirst() {
    let url = URL.init(fileURLWithPath:filename)
    do {
        try FileManager.default.evictUbiquitousItem(at:url)
    } catch let error as NSError {
        print("\(filename): \(error.userInfo)")
    }
}

SWIFT

This neat little package has:

  • turned our Swift script into a heredoc (<<SWIFT to kick it off, and SWIFT on its own line to end it);
  • fed it to the Swift interpreter (/usr/bin/env swift is a time-honored trick for finding a program on your PATH) as standard input;
  • told swift to expect a script on standard input (-); and
  • passed through all the shell arguments to the Swift interpreter so the script can use them ("$@").

And that’s all we need to do! If you want to test it before installing, you can add an “Ask for Finder Items” action immediately above the script, and you’ll be prompted for files before the script runs.

Your new Quick Action will be bundled up in a .workflow bundle, which you can share. I’ve put my version on GitHub.

When someone double-clicks it, Automator Installer will install it by default. (You can find installed Quick Actions in Library/Services in your home folder.)