How to create file previews in macOS

Update My PreviewMarkdown app, which provides Markdown file previews and icon thumbnails in Catalina, is now available from the Mac App Store.

macOS Catalina introduces a new mechanism for providing file previews and content-based file icons. The system for doing this is still QuickLook, but the standalone or app-hosted QuickLook generators that have been in use for some time have been deprecated in favour of delivering this functionality through app extensions.

A Markdown preview in Catalina, courtesy of PreviewMarkdown

QL generators — found in ~/Library/QuickLook and /Library/QuickLook — will still work, but expect macOS 10.16 or 10.17 to remove them if Apple stays true to form. With Catalina, they will need to be signed by a developer ID. I presume the new, app-centric methodology is intended to make preview generation more secure: apps can be easily signed and notarized, on-board extensions and all.

Apple’s documentation for the new-style QuickLook operation is a little short on details, so here are my experiences of digging into the new preview and thumbnail mechanism. The API is essentially the same as the one that’s been available on iOS for some time, but here’s how it applies to macOS.

To add an extension to an app, just add a new target to the app’s Xcode project. Xcode has templates for both QuickLook Preview and Thumbnail extensions, and selecting either of these provides you with the core code resources you need. In this post, I’ll focus on previewing; I’ll cover thumbnailing in another post later.

The template contains a file called PreviewViewController.swift which includes a class called PreviewViewController, which is an NSViewController sub-class that adopts the QLPreviewingController protocol. This protocol mandates one property and two functions. The property provides the instantiating code with a way to access the PreviewViewController’s NIB file (also included in the template) via its name. The key function, preparePreviewOfFile(at, completionHandler), is called by QuickLook when it needs the extension to generate a file preview.

The idea is that since your app is in charge of its own data structures, it’s the thing that’s best placed to read one if its files and produce a preview of it. The extension does this by adding the preview as a view to the PreviewViewController instance’s primary view. I say “a view” because this means you can prepare a PreviewViewController view that presents a complex UI combining many sub-views. That said, the more complex the preview view, the longer it will take to present to the user who has just selected an icon and hit Space.

Or has Finder’s preview pane open. Or has the Preview section of if the file info panel showing. Or in the Spotlight search results dialog. Or in Catalina’s Open… dialog. macOS uses QuickLook to populate all of these places, and more, not just the familiar pop-up preview.

The first parameter of preparePreviewOfFile(at, completionHandler) is passed the to-be-previewed file’s URL, accessed in the body of the function as url. The second parameter receives a function from QuickLook. Your code calls this function, as handler(), when it has prepared the preview and is ready to hand control back to QuickLook, which grabs the view you’ve built and drops it into whichever bit of the macOS UI is displaying the preview.

The basic methodology for the function therefore is:

  1. Load the file located at url.
  2. Render the file’s data into an NSView or sub-class of NSView.
  3. Add the NSView to the PreviewViewController’s view, either programmatically or via an Interface Builder outlet you’ve hooked up to the NIB.
  4. Call handler(), passing in either nil to indicate the preview is ready or an Error structure to tell QuickLook that a preview can’t be created.

That’s the code; you also need to configure your extension, a process that involves updating its Info.plist to indicate what file types, specified as URIs, it handles. The plist already has an NSExtension entry with an NSExtensionAttributes dictionary. One of the dictionary entries is QLSupportedContentTypes, which has an array value: double-click this value (“0 items”) to begin adding strings for each of your app’s UTIs.

Build your app and run it; QuickLook will register the presence of the preview extension and make use of it when a user previews a document of the correct type. You can see that the previewer is registered with the system by looking under Quick Look in System Preferences > Extensions.

Enable or disable extensions in System Preferences

Debugging extensions can be tricky: Xcode breakpoints are not triggered because the code runs outside of the debugger, but judicious use NSLog calls can help you monitor the code’s progress via Console.

The second function auto-added to your PreviewViewController so that it supports the QLPreviewingController protocol is preparePreviewOfSearchableItem(identifier, queryString, completionHandler). My code currently just logs the identifier (a string) and the query string (which is a string optional) as I attempt to learn when this function is called. Apple does not say.

Update But it does include a key, QLSupportsSearchableItems, in the extension’s Info.plist file. This has a Boolean value, which is NO initially. I will try setting this to YES and see what happens…

If you’re running Catalina, you can try this out by downloading and installing my app PreviewMarkdown, which you can find here. The source code will follow shortly, in GitHub.