Quick Look Preview Extensions on macOS: Data Replies and View Controllers
Build custom macOS Quick Look previews with a data reply or view controller, declared content types, bounded file work, and tested failure paths.
A Quick Look Preview Extension teaches the system how to preview a file type your app supports. That is a different integration from placing the shared QLPreviewPanel inside an AppKit app, and it is different again from generating Finder thumbnails. The extension path begins with a declared content type and ends with either a prepared view controller or a QLPreviewReply.
Apple documents two implementation styles for custom types: a view-based preview and a data-based preview. Choose based on the product you need to render, not on whichever sample is shortest. A static report that can be transformed into HTML, PDF, or an image is a good data-based candidate. A preview that needs an AppKit view hierarchy and its own presentation logic belongs in a view controller. Both approaches still need a small, predictable contract between the type declaration, extension entry point, parser, and displayed result.
Declare only the file types the extension can handle
The extension’s QLSupportedContentTypes array declares the content types it supports for preview. Use the identifiers that match the formats the extension actually handles; a filename suffix is not a substitute for a content-type identifier. Keep the list narrow. If two formats share a suffix but have different structures, test them as separate inputs rather than assuming the extension can infer their meaning from the name.
For a data-based implementation, set QLIsDataBasedPreview to true and list the supported type identifiers in QLSupportedContentTypes in the extension’s Info.plist. The example places these values in NSExtensionAttributes under NSExtension, following Apple’s general app-extension key structure; the Quick Look API reference documents the values, while the archived key reference documents the generic parent-child layout. The principal class and Quick Look preview extension-point identifier are direct children of NSExtension. This minimal property-list fragment shows that conventional placement; it omits other bundle metadata and is not a complete app manifest. Replace the sample identifier with a type identifier defined by your app:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>QLIsDataBasedPreview</key>
<true/>
<key>QLSupportedContentTypes</key>
<array>
<string>com.example.project-manifest</string>
</array>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.quicklook.preview</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ManifestPreviewProvider</string>
</dict>
</dict>
</plist>
The principal class must be the class actually built into the extension target. A typo or module-name mismatch is a packaging defect, not a rendering failure. Keep this configuration beside the target that owns the extension, then inspect the built extension’s Info.plist during release validation instead of trusting a source plist that may not have been copied into the product.
Choose a data reply when your renderer can produce a bounded artifact
For a data-based extension, subclass QLPreviewProvider and conform the subclass to QLPreviewingController. Implement providePreview(for:completionHandler:) or its async throwing form. The system supplies a QLFilePreviewRequest; its fileURL identifies the content to preview. The provider creates a QLPreviewReply representing output such as HTML, PDF, or an image.
Here is a Swift skeleton for a text-based manifest format. ManifestReader and escapeAndFormatHTML are application code, not Quick Look APIs; their contract is to inspect a bounded amount of input and return escaped display text. Keep parsing limits and format-version checks in that layer.
import Foundation
import QuickLookUI
import UniformTypeIdentifiers
final class ManifestPreviewProvider: QLPreviewProvider, QLPreviewingController {
func providePreview(for request: QLFilePreviewRequest) async throws -> QLPreviewReply {
let summary = try ManifestReader.readSummary(
from: request.fileURL,
maximumBytes: 64 * 1024
)
let html = escapeAndFormatHTML(summary)
return QLPreviewReply(
dataOfContentType: .html,
contentSize: CGSize(width: 720, height: 520)
) { _ in
Data(html.utf8)
}
}
}
The byte limit is an application policy in this example, not a Quick Look limit. A real parser should reject malformed headers, unsupported format versions, and values outside its own safe bounds. If a document is large, do not load it all merely to display its title and summary. If output is HTML, escape values originating in the document and include only resources your renderer controls. The extension should render a useful preview from the file itself without relying on a network lookup or an already-running editor window.
The QLPreviewReply initializer accepts a UTType, a content size, and a closure that produces Data. Select a content type that matches the bytes the closure returns. Do not label arbitrary text as PDF, or return an HTML body while claiming a different type. When the result already exists as a supported file such as a PDF or image, QLPreviewReply(fileURL:) is another documented construction path. Apple describes this initializer as taking an existing file URL but does not specify a temporary-file cleanup boundary; keep any generated file available for the preview operation and choose an explicit cleanup point in your implementation.
The callback and async entry points have different error signatures: the former reports through an error-or-reply completion handler, while the latter is async throws. Apple documents those API shapes, but the cited macOS API pages do not establish a general-preview guarantee for either failure path. Return or throw a meaningful failure instead of manufacturing a misleading success, then test the visible result on every supported macOS release. Log the format version, coarse failure stage, and error category where useful, but do not log document contents or private paths.
Use a view controller when the preview itself is an AppKit presentation
A view-based extension’s principal class is an NSViewController conforming to QLPreviewingController. For a file-URL preview, implement preparePreviewOfFile(at:completionHandler:); the protocol also supports preparing a preview from a Spotlight searchable item. The Quick Look UI overview describes preparing and displaying the view in the file preparation method.
Apple’s current method documentation says the operating system calls preparePreviewOfFile(at:completionHandler:) once from the main thread before presenting the previewing controller. The system displays a loading spinner, and the handler may run asynchronously after the method returns. Keep the main-thread portion limited to establishing the view and presenting already-prepared state; do not perform long-running or resource-intensive work there. The documentation also advises against holding a file descriptor open for the duration of the preview. Read what is needed, close the resource, then let the preview remain a view rather than a live handle to the source file.
Use this style for a layout that genuinely needs an AppKit view hierarchy. Do not select it merely to wrap a static image or HTML document in a custom controller. Conversely, do not force a complex, stateful presentation into a generated HTML string solely because QLPreviewReply is convenient. Keep the implementation focused on previewing; editing, saving, or mutating the source remains the responsibility of the containing application.
Bound work and make failure observable
Quick Look requests are triggered by a system surface, so treat the input as an external document even when your own app normally creates it. Check that the URL can be read, parse the expected version, enforce format-specific size and nesting limits, and cap generated output. Avoid unbounded decompression, recursive references, huge embedded images, and synchronous network access. Those are defensive implementation choices rather than framework-enforced quotas, and the limits should be chosen from the format your app supports.
Keep data-based generation deterministic: the same valid input and renderer version should produce the same visible summary. For a view-based result, separate parsing from view updates so a malformed file can report a clear error rather than leaving a half-populated interface. Keep a small neutral error path; do not expose parser internals, stack traces, or source contents in the preview UI.
During development, test the built extension product, not just the source target. Confirm that the supported content-type identifiers and principal-class setting are present in its Info.plist, and that the deployed target uses the intended implementation style. Availability needs particular care: Apple’s macOS guide directs apps targeting macOS 10.15 and later to preview extensions rather than legacy generators, and WWDC 2019 describes the view-based QLPreviewingController path. Apple’s current method page lists preparePreviewOfFile(at:completionHandler:) as macOS 10.10, while the protocol overview’s symbol metadata reports macOS 12.0; treat that overview/method discrepancy cautiously and check the actual declaration in the SDK used for your minimum target. By contrast, Apple’s data-based QLPreviewProvider, QLFilePreviewRequest, and QLPreviewReply APIs are marked macOS 12.0 or later. Do not infer the data-based API floor from the older view-based workflow, or infer view-based availability from one conflicting protocol summary.
Acceptance tests for both implementation styles
Exercise the preview through the actual Quick Look path for each supported type. A successful compile proves neither that the system associates the file with the extension nor that the renderer can handle real documents. Keep small fixtures for the nominal format and for its boundary cases.
| Test input or condition | Verify |
|---|---|
| Valid file for each declared content type | The expected extension path runs and the result matches the document’s content. |
| Type not declared by the extension | The extension does not claim a format it does not parse; verify the system’s fallback behavior on the supported macOS release. |
| Empty, truncated, malformed, and unsupported-version files | Parsing fails safely and the implementation reports the error through its documented return path. |
| Large file and oversized embedded content | Parsing and generated output stay within the limits your format implementation defines. |
| Unavailable or unreadable source file | The preview does not display stale data from a previous request. |
| HTML containing markup-like document text | Values are escaped and cannot become unintended active markup. |
| Data-based reply | The UTType matches the reply bytes and the declared contentSize matches the renderer’s intended canvas. |
| View-based controller | Preparation is responsive, and the source file is not held open for the preview’s entire display lifetime. |
For the callback API, test both a valid QLPreviewReply and an error result. For the async API, test the visible result of a thrown error on each supported OS. Do not assume a system-generated generic preview, or identical failure presentation between overloads, unless that behavior is confirmed on the target macOS release. Repeat with a clean install of the signed app so target configuration and extension packaging are part of the test, not an assumption inherited from a developer machine.
A reliable custom Quick Look extension is a small renderer behind an explicit type declaration. The data-based path turns a request URL into a bounded supported reply; the view-based path prepares a focused controller without blocking the main thread or pinning the source file open. Keeping that contract separate from panel ownership and thumbnail generation makes failures easier to reproduce and the preview behavior easier to maintain.
Related:
- Quick Look Preview Panels on macOS: Responder Ownership and Item Lifetimes
- Quick Look Thumbnailing on macOS: Requests, Extensions, and Cache Boundaries
Sources:
- Apple Quick Look UI framework overview
- Apple App Extension Info.plist keys and
NSExtensionstructure - Apple
NSExtensionPointIdentifier: Quick Look preview extension point - Apple
QLPreviewProvider: data-based extension configuration and principal class - Apple
QLPreviewingController: view-based preparation and preview methods - Apple macOS 10.14-and-earlier Quick Look guide, with guidance for 10.15-and-later extensions
- Apple WWDC 2019: What’s New in File Management and Quick Look
- Apple
QLFilePreviewRequest: request URL - Apple
QLPreviewReply: supported data-based reply construction - Apple
providePreview(for:): Swift async-throws signature - Apple
preparePreviewOfFile(at:completionHandler:): view preparation lifecycle