Quick Look Thumbnailing on macOS: Requests, Extensions, and Cache Boundaries
Build responsive macOS thumbnails with QuickLookThumbnailing requests, bounded custom providers, content-type declarations, and stale-result handling.
Quick Look thumbnails and Quick Look previews solve related but different problems. A thumbnail is a compact visual representation used in grids, Finder, and other system surfaces. A preview is a larger interactive presentation of a file. This article focuses on the QuickLookThumbnailing framework: generating thumbnails inside an app and providing a Thumbnail Extension for custom file types. It does not describe preview extensions or preview UI.
The key operational point is that thumbnail production is asynchronous and demand-driven. A file browser may request many sizes while the user scrolls, cancel work for cells that leave view, and request the same item again at a different scale. Treat each request as bounded work that can fail or become stale, not as a one-time initialization call.
Generate thumbnails for files your app displays
QLThumbnailGenerator can generate a representation for a file URL using a request that specifies size, display scale, and acceptable representation types. Choose the pixel dimensions from the destination view’s point size and backing scale rather than asking for an unnecessarily large image. A QLThumbnailRepresentation exposes a Core Graphics image and, when AppKit is linked, an NSImage.
Use the shared generator for ordinary requests and retain enough state to cancel work that is no longer relevant. A request is not a promise that a thumbnail will be available; the file can disappear, access can be denied, the format can be unsupported, or generation can fail. Keep a deterministic placeholder and a retry policy appropriate to the UI.
import AppKit
import QuickLookThumbnailing
final class ThumbnailLoader {
private let generator = QLThumbnailGenerator.shared
private var request: QLThumbnailGenerator.Request?
func load(url: URL, points: CGSize, scale: CGFloat,
completion: @escaping (NSImage?) -> Void) {
cancel()
let request = QLThumbnailGenerator.Request(
fileAt: url,
size: points,
scale: scale,
representationTypes: .thumbnail
)
self.request = request
generator.generateBestRepresentation(for: request) { representation, error in
guard error == nil, let representation else {
DispatchQueue.main.async { completion(nil) }
return
}
DispatchQueue.main.async { completion(representation.nsImage) }
}
}
func cancel() {
if let request {
generator.cancel(request)
}
request = nil
}
}
This pattern demonstrates request ownership and callback handoff. A reusable collection cell should also carry a generation token or the requested file identity so a slow result for an earlier item cannot repaint a cell after reuse. Add explicit handling for cancellation, main-actor isolation, and file-access scope in the actual application. Do not block a scrolling callback waiting for the completion handler.
Respect point size, scale, and representation choice
The request’s size is expressed in points and its scale describes pixel density. Confusing the two can request a thumbnail that is too small on a high-density display or much larger than needed. For a 120-point square preview at a backing scale of 2, the request describes those two values separately. Measure the actual AppKit view and refresh the request when its size or display scale changes.
QuickLookThumbnailing can produce different representation types. Request only what the view uses and select the best representation API when progressive updates are not required. If the UI can show an early small representation and replace it with a better one, use the multiple-representation API and associate each callback with the same request generation. Do not let a later low-quality callback overwrite a more appropriate final image.
The returned thumbnail includes a content rectangle that identifies the region representing the document within the thumbnail. This can matter when fitting or cropping in a cell. Use the representation and its metadata rather than assuming every file fills the full bitmap. If the design intentionally crops, do so at a later presentation layer and keep the original representation available for alternate layouts.
Provide thumbnails for a custom file type
If a file type is custom, Finder and other system clients may show a generic icon unless the installed app provides a Thumbnail Extension. Add the extension target, subclass QLThumbnailProvider, and implement provideThumbnail(for:). The extension receives a QLFileThumbnailRequest with the file URL and size constraints; it returns a QLThumbnailReply with a drawing block or an image-file URL.
The extension’s Info.plist declares exact supported Uniform Type Identifiers in QLSupportedContentTypes. A parent type is not necessarily enough: Apple’s guide specifies that the listed UTI must match the file type for the extension to be used. The optional minimum-dimension setting tells the framework not to invoke the extension for requests smaller than the extension can represent well, in which case the system can provide a generic file icon instead.
import AppKit
import QuickLookThumbnailing
final class ProjectThumbnailProvider: QLThumbnailProvider {
override func provideThumbnail(
for request: QLFileThumbnailRequest,
_ handler: @escaping (QLThumbnailReply?, Error?) -> Void
) {
let reply = QLThumbnailReply(
contextSize: request.maximumSize,
currentContextDrawing: {
guard let context = NSGraphicsContext.current?.cgContext else {
return false
}
context.setFillColor(NSColor.windowBackgroundColor.cgColor)
context.fill(CGRect(origin: .zero, size: request.maximumSize))
return self.drawIllustrativeThumbnail(at: request.fileURL,
size: request.maximumSize,
in: context)
}
)
handler(reply, nil)
}
private func drawIllustrativeThumbnail(at url: URL, size: CGSize,
in context: CGContext) -> Bool {
guard FileManager.default.fileExists(atPath: url.path) else { return false }
context.setFillColor(NSColor.systemBlue.cgColor)
context.fill(CGRect(x: 12, y: 12, width: max(0, size.width - 24),
height: max(0, size.height - 24)))
return true
}
}
The helper draws a deliberately simple placeholder after confirming the source exists. A real extension should create an informative thumbnail from a bounded amount of data, handle malformed documents, and call the completion handler exactly once. Use the Core Graphics drawing-block variant instead if the rendering code is written around a supplied CGContext; do not mix the coordinate assumptions of the AppKit current-context initializer with a Core Graphics initializer.
Keep the extension cheap, deterministic, and self-contained
Thumbnail generation must not require opening the full editor or waiting on the network. Parse only the header or bounded metadata needed to draw a useful image. If a document is large, stream or seek to the needed portion rather than reading the entire file into memory. If the file is encrypted or unavailable, return an error or a neutral thumbnail instead of exposing protected content.
A Thumbnail Extension is a separate extension target with its own bundle, supported content types, resource budget, and availability constraints. Do not assume it shares the main app’s in-memory caches or launch state. Include all required resources in the extension bundle or derive the image from the request file. Keep work independent of the active UI, and avoid code paths that assume a window or a user-selected document is already open.
Treat request.maximumSize and minimumSize as real constraints. Do not produce an image whose context is arbitrarily huge and rely on the system to scale it down. Choose the closer context size that satisfies the request and draw within that coordinate space. If the document can be rendered at many aspect ratios, define a consistent fit or crop rule and test its content rectangle.
File URLs, coordination, and remote content
The request URL may refer to a file in a provider-managed location, not a stable local file that is already materialized. For app-owned browsing, request thumbnails only when a row is close enough to display and respect any security-scoped access or file-coordination requirements imposed by the surrounding file workflow. Avoid holding file access longer than necessary. A thumbnail failure should not be interpreted as proof that the document is corrupt.
For a File Provider domain, the system and provider control placeholder materialization and thumbnail exchange. Do not start an independent bulk downloader just to make thumbnail rows appear. Where the File Provider contract supports it, use the appropriate thumbnail representation pathway and manage temporary output files as documented. The saved-representation API exists primarily for file-provider extensions with memory limits; its output file is the app’s responsibility to delete when no longer needed.
For a remote or offline item, define a placeholder state distinct from “there is no thumbnail.” This lets the UI communicate loading, temporary unavailability, or an unsupported file type without repeatedly issuing futile requests. Apply bounded concurrency to large grids and cancel requests for rows no longer needed. A cache should be keyed by stable file identity plus modification/version information, target dimensions, and representation policy, not merely by a display name.
Common failure cases
If Finder never calls the provider, verify that the extension is embedded, activated, and declares the exact file type identifier. Confirm the extension scheme is built and installed for testing, and inspect the provider’s logs rather than changing unrelated Launch Services registrations. If only some files fail, test their declared content types and validate that the file parser handles those versions.
If a generated thumbnail is blank, distinguish a false drawing-block result, a missing current graphics context, a zero-size request, a bad coordinate transform, and a parser failure. If a thumbnail is blurry, compare requested point size and scale with the backing bitmap dimensions. If cells show images for the wrong files, attach a request generation and identity and ignore stale completions.
If the host process or extension becomes memory-heavy, look for decoded full-resolution images retained after the thumbnail is produced, duplicate in-flight requests, and contexts or caches that outlive their intended use. Bound parallel work, cancel obsolete tasks, and use a small output buffer. Do not store every thumbnail from a folder permanently in RAM.
Acceptance checks
Exercise a local file, a cloud placeholder, a missing file, malformed content, a large document, a high-DPI request, and repeated cancellation. For the extension, validate its embedded Info.plist, exact type declarations, minimum dimension, and output at multiple requested sizes. Check the actual output’s pixel dimensions, aspect handling, and memory use. Simulate rapid scrolling and verify that late callbacks cannot replace newer cells.
QuickLookThumbnailing supplies a system request pipeline and an extension point. It does not define your document’s visual identity, file-access rules, cache key, or UI stale-result policy. Make those explicit, then measure on real Finder and in-app request paths rather than trusting one successful preview.
Related:
- File Provider on macOS: Domains, Placeholders, and System-Managed Sync
- NSDocument on macOS: Lifecycle, Autosave, and Version Recovery
Sources: