How to share preferences between macOS/iOS apps

A couple of macOS releases or so ago, Apple introduced app extensions: self-contained modules that are bundled within apps to deliver functionality to the wider operating system. But how do apps and their extensions share information between themselves, in particular users’ preferences?

PreviewMarkdown’s new Preferences sheet

Here’s a real app extension scenario. PreviewMarkdown, my QuickLook file preview and Finder thumbnail generator for Markdown files on macOS Catalina and above, delivers these features through a pair of app extensions, one to render the previews, the other to create icon thumbnails. These extensions are delivered within a host app.

When Finder needs to generate icon thumbnails, usually when you open a Finder window, it activates the registered PreviewMarkdown app extension and passes it the URL of the file it needs a thumbnail for. The app extension renders an image of the file and hands it back to Finder.

I use app extensions because Apple has deprecated the old QuickLook generator approach — based on .qlgenerator applets — in favour of app extensions, so we should expect QuickLook generators to stop working in a future macOS release. If they’re not code-signed and notarised, they won’t work now, at least not without resorting some some potentially insecure hackery.

A user recently got in touch through PreviewMarkdown’s feedback system to ask if I could make the preview text smaller. No problem — I’d just need to add a preferences panel and add that as a settings. While I was at it, I built the Preferences panel show above. That’s straightforward — here’s the tricky part: how can preferences set by the main app at any time be accessed by the preview extension when it gets loaded by macOS’ QuickLook daemon in response to a user selecting a Markdown file and hitting the space bar?

The answer is app suites, a macOS (and iOS too) mechanism for sharing preferences among related applications. No doubt this feature was originally intended to support apps from one developer, with common settings, like an office suite or such. It works along the lines of standard app preferences, which are implemented using NSDefault. Rather than using the standardDefaults property, you make use of a suite name:

UserDefaults(suiteName: YOUR_SUITE_NAME)

The suite name is a string, and the first one I tried was "suite.preview-markdown".

UserDefaults() returns an optional value, so you unwrap it and read your settings values, in your main app or any of your extensions, in the usual way:

if let defaults = UserDefaults(suiteName: YOUR_SUITE_NAME) {
    self.previewBodyFont = defaults.integer(forKey: "body-font-index")
}

You write the settings back, in your main app, like this:

defaults.setValue(self.bodyFontPopup.indexOfSelectedItem, 
                  forKey: "com-bps-previewmarkdown-body-font-index")

You register your preferences at the start this way:

let bodyFontDefault: Any? = defaults.object(forKey: "body-font-index")
if bodyFontDefault == nil {
    defaults.setValue(BODY_FONT_INDEX_DEFAULT, 
                      forKey: "body-font-index")
}

That’s to say, you check if the setting is present. If it isn’t, you write the initial, default value. If the setting has not yet been applied, then the optional returned by defaults.object(forKey:) will be nil and so you know you need to add a default value.

Finally, you can call defaults.synchronize() to persist the settings there and then, rather than when the OS gets around to it. This ensures that they are immediately available to the app extensions, which read in the values they are interested in as shown above.

There’s one more thing you need to do: add an App Suite entitlement to the app and each of its app extensions. This takes as its value the same suite name.

Straightforward, it seemed, but the first time I tried it, I couldn’t get it to work. I checked that I had applied the entitlement throughout and that I had used the suite name consistently. I had, but still no joy.

After a little experimentation with different suite names, it turns out that the key component to making app suites work is to use your unique Developer Account ID as a prefix. This is how Apple’s documentation presents app suite ID, and what Xcode defaults too, but the write-up implies that this is optional. In fact, you must include this ID for it preference sharing to work. Once I added it in, everything clicked into place.

The upshot is that PreviewMarkdown’s main app now allows you to apply a basic set of preferences — font size, font style and the colour of code, for example — and the app extensions will apply them when they next generate Markdown previews or thumbnails. Finder only re-renders thumbnails in certain circumstances, so you may not see thumbnails change immediately, but previews will make use of the new settings as soon as the are triggered.

So a big thank you to the anonymous feedback provider who suggested adding preferences to PreviewMarkdown, without whom the app wouldn’t have a set of useful preview and thumbnail customisations — and I wouldn’t have explored app suites.

You can download PreviewMarkdown 1.2.0 from the Mac App Store.