Quick Look Preview Panels on macOS: Responder Ownership and Item Lifetimes
Integrate QLPreviewPanel with AppKit's responder chain, retained preview models, stable file access, reloads, and explicit teardown.
Quick Look on macOS has two distinct roles. A Quick Look Preview Extension teaches the system how to preview a custom file type, while QLPreviewPanel displays preview items inside an AppKit application. This article focuses on the panel path. It does not replace thumbnail generation or implement a preview extension; it covers the ownership and responder-chain contract for showing an existing file or app-owned preview item to the user.
An application has one shared QLPreviewPanel. The panel follows the responder chain and asks responders whether they accept control. The first suitable responder becomes the panel’s controller and provides items through QLPreviewPanelDataSource. This means panel ownership is contextual: the window or document currently handling preview should control the shared panel, not an unrelated global singleton that outlives every document.
Model preview items explicitly
A preview item supplies a title and URL. Keep the corresponding item model alive while the panel uses it, and ensure the URL remains readable for the required preview operation. Do not treat a display name as file identity. A temporary export URL, a coordinated document URL, and a user-selected security-scoped URL have different lifetimes and access requirements.
import AppKit
import QuickLookUI
final class FilePreviewItem: NSObject, QLPreviewItem {
let previewItemURL: URL?
let previewItemTitle: String?
init(url: URL, title: String) {
previewItemURL = url
previewItemTitle = title
super.init()
}
}
final class PreviewItems: NSObject, QLPreviewPanelDataSource {
var items: [FilePreviewItem] = []
func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
items.count
}
func previewPanel(_ panel: QLPreviewPanel!,
previewItemAt index: Int) -> QLPreviewItem! {
guard items.indices.contains(index) else { return nil }
return items[index]
}
}
The data source object must itself be retained by the controller that owns the preview session; do not rely on a weak delegate/data-source property to keep it alive. Validate the requested index because the item list can change while the shared panel is open. A real controller should also participate in the responder-chain methods that take and relinquish panel control.
Treat panel availability as a presentation concern, not as proof that the backing resource is authorized or durable. Resolve file-provider or document URLs through the application’s existing access path, preserve any required access scope while the user is previewing, and release that scope when the preview session ends. Keep this policy outside the data source callback so an item lookup remains deterministic and does not begin unbounded I/O or silently extend access.
Responder-chain ownership
When the user invokes Quick Look from a document window, the relevant window controller or view controller can accept control while it is active. When another window becomes key, the panel may route control to a different responder. Implement the documented acceptance and begin/end control methods so the item list is switched deliberately and panel callbacks do not continue to reference a closed document.
Do not subclass QLPreviewPanel; Apple documents that the panel is not subclassable. Use its delegate and data source protocols for supported customization. Avoid building a second overlapping preview window that races with the shared panel unless the product has a separate, explicit preview experience.
Panel presentation and data-source ownership are separate. The data source answers count and item lookup; the responder that controls the panel owns its current context and decides when to reload. Keep those responsibilities together in a controller or a small preview coordinator.
Refresh and replacement
If the selected file changes while the panel remains open, update the data source’s stable item list and ask the panel to reload or refresh the current item as appropriate. Do not assume assigning a new item means its content is ready immediately; Quick Look loads previews asynchronously. Disable or adjust actions while the selection is being replaced, and guard against a stale preview finishing after a newer item has become current.
Use a generation number when preview requests or file exports are asynchronous. When a completion returns, compare its generation and URL identity to the current selection before publishing it. If the source file was replaced at the same path, compare a stable content revision or file identity instead of only the string path.
When a preview item’s URL is generated temporarily, keep the file available through the preview lifecycle and remove it only after the panel no longer needs it. Avoid rewriting the file underneath the panel while it is being read. For coordinated document access, follow the document’s file coordination strategy rather than racing a writer and Quick Look reader.
Selection, navigation, and panel state
Represent the preview selection as an ordered list of stable items and an index into that list. When a selected item is removed, choose whether the preview closes or moves to an adjacent surviving item. Keep the list and index consistent before calling reloadData(). An index alone is not durable state because sorting or filtering can reorder the array.
For a multi-item preview, preserve user navigation only when the same item still exists after a refresh. If the collection changes, find the old item by identity and compute its new position. Avoid using a localized title to match items because titles can be duplicated or edited.
The panel has presentation behavior and full-screen support that the application should not fake by creating a custom overlay. Use the documented properties and delegate methods, and test the panel when other windows or documents become active. Ensure a keyboard command intended for another window does not accidentally manipulate the preview panel’s stale item list.
File access and failure behavior
Previewing may read a file asynchronously. Keep security-scoped access active as required for a user-selected URL and coordinate reads for documents that can change concurrently. Do not assume a URL can be accessed merely because it was valid when the item was created. If a file is deleted or permission expires, replace the stale item with a recoverable error state or close the preview rather than leaving a broken panel indefinitely.
Do not hold a file open for the entire preview duration unless an interaction requires it. Apple’s Quick Look preview guidance recommends opening only for the duration needed by an interaction. Close file handles on every success, failure, and cancellation path. For app-generated previews, publish an immutable staging artifact and avoid modifying its contents while Quick Look is reading it.
Accessibility and navigation
The preview panel handles common file interactions, but the surrounding application still needs accessible names for the preview command and the selected item. Keep the title accurate and expose the source document in context. Test keyboard navigation, VoiceOver, and actions that move between items. A row selection in the source table should remain consistent with the panel’s current preview item.
If users can preview a file without opening it, do not imply that the document was imported or saved. A Quick Look panel is a viewer. Opening, editing, or committing a file is a separate action with its own lifecycle and error handling.
Acceptance tests
Test one item, multiple items, duplicate titles, empty list, selected item deletion, item reorder, source URL unavailable, temporary-file replacement, a second window taking panel control, controller deallocation, and a preview callback completing after the selected document closes. Assert that the panel always resolves its current item from the active controller’s retained source list.
Log panel owner identifier, item ID, URL class (not sensitive path), generation, reload reason, and open/read failure. Avoid logging document contents. Verify that the data source is alive for the panel lifetime and that observers and temporary files are released after control ends.
The reliable Quick Look panel integration treats the panel as a shared responder-chain resource, retains its current item models, and ties file access to a bounded lifecycle. AppKit and Quick Look render the preview; the application owns which item is current and whether it is still valid.
Related:
- Quick Look Thumbnailing on macOS: Requests, Extensions, and Cache Boundaries
- AppKit Responder Chains: First Responder, Actions, and Keyboard Routing
Sources: