Skip to content
macOSDeep Dive Published Updated 9 min readViews unavailable

NSPasteboard on macOS: Typed Data, Lazy Providers, and File Promises

Implement robust macOS copy, paste, and drag workflows with typed pasteboard items, lazy representations, change tracking, and file promises.

The macOS pasteboard is a system-mediated transfer channel, not a private string variable shared between two views. Applications use NSPasteboard to exchange typed representations for copy and paste, Services, and drag and drop. One logical object may offer several formats; the receiving app chooses a representation it understands. Large or generated data can be supplied lazily, and a file promise can defer creating a file until a drag destination actually requests one.

Treat a pasteboard operation as a negotiation over types and ownership. The provider may disappear, the contents may change, the receiver may prefer another representation, and a promised file may not exist until a later callback. Correct code offers accurate types, retains its provider long enough, validates what it reads, and never blocks the main thread generating a large representation.

The general clipboard and drag pasteboard are different

NSPasteboard.general is the ordinary system clipboard. A drag operation uses a drag pasteboard associated with the current drag session. Do not read the general clipboard to infer what a drag contains or write a private drag payload to the user’s clipboard. AppKit’s drag-and-drop callbacks provide the pasteboard associated with that interaction.

A pasteboard can contain multiple items, and each item can expose multiple representations. NSPasteboardWriting describes how an object can supply one or more representations; NSPasteboardReading describes how a receiver can initialize an object from supported types. Prefer these protocols or explicit NSPasteboardItem instances over legacy first-item-only APIs when the transfer has multiple items or needs lazy representations.

For a small text clipboard value, a direct item is straightforward:

import AppKit

func copyText(_ text: String) -> Bool {
    let item = NSPasteboardItem()
    guard item.setString(text, forType: .string) else { return false }

    let pasteboard = NSPasteboard.general
    pasteboard.clearContents()
    return pasteboard.writeObjects([item])
}

func readText() -> String? {
    NSPasteboard.general.string(forType: .string)
}

The write replaces the clipboard contents, so call it only in response to an intentional copy action. A production paste action should negotiate supported types and report a read or conversion error rather than assuming the board contains a string. For custom types, use an appropriate Uniform Type Identifier and document the payload schema; a type name alone does not validate bytes.

Type negotiation and multiple representations

Offer representations that preserve useful fidelity and interoperate with other Mac apps. A rich-text editor might offer its native document format, an attributed string, and plain text. An image editor might offer a native project representation plus TIFF or PNG. Make every advertised representation valid and consistent with the same logical selection. If exporting a simpler format loses information, that loss should be intentional and tested.

On receive, ask whether the pasteboard can provide a supported class or type before attempting to build a large object graph. readObjects(forClasses:options:) is useful when the app accepts standard Cocoa objects. For custom schemas, read a specific NSPasteboardItem type and validate size, encoding, and required fields before creating domain objects. Do not trust a filename, extension, or UTI to prove that bytes are safe or well-formed.

Multi-item ordering and grouping matter. Preserve one pasteboard item per logical exported object when the receiver should be able to distinguish items. Do not flatten a list of unrelated files into one ambiguous data blob. Test the receiver path with a single item, several items, mixed supported types, and a source that offers only a fallback representation.

Lazy data providers and ownership lifetime

If materializing a representation is expensive, an NSPasteboardItemDataProvider can provide data when the receiver requests a particular type. The provider is asked to fulfill a requested type and receives a callback when the pasteboard no longer needs it. This can avoid eagerly encoding every representation even when the receiver will consume only one.

Lazy provision introduces a lifetime contract. Keep the provider and its source snapshot valid until AppKit indicates that it no longer needs the data. Capture immutable data or an immutable export snapshot rather than reading mutable UI state later, after the user’s selection may have changed. If the selection changes, create a new pasteboard item/provider for the new ownership epoch instead of allowing an old request to export a different object than the one the user copied.

Use changeCount to detect whether pasteboard ownership changed since the app declared or wrote contents. This is useful for deciding whether the app still owns delayed data. It is not a lock and does not freeze the clipboard while the app reads it. When a read depends on a previously observed clipboard state, capture the change count, read the needed representations, and re-check if the distinction matters; handle a mismatch as a stale or changed clipboard rather than assuming an atomic snapshot.

Keep reads user-initiated and avoid polling the general clipboard. The pasteboard is shared across applications and may contain sensitive user data. Platform privacy behavior can surface access to the user and can be configured per app. A background timer that repeatedly inspects clipboard contents is both unnecessary for normal copy/paste and a poor user experience.

File URLs are not file promises

A file URL identifies a file that exists at a location. A file promise describes a possible future file, often because the source does not want to render or write the file until the destination accepts the drag. These are different transfer contracts. If an app supports external drags, accept file URLs when appropriate and also register for promised file types where the source ecosystem can provide them.

An NSFilePromiseProvider represents one promised output file. Set its file type and delegate before putting it on the pasteboard. The type must be a supported UTI that conforms to data or directory. The delegate supplies a filename and writes the promised contents to the destination URL when the drop is complete. Provide an operation queue that is not the main operation queue so large file generation does not freeze the UI. The delegate must call its completion handler on both success and failure.

The destination URL belongs to the promise fulfillment operation. Write to the provided URL; do not invent a path based on the current directory or assume the destination has already read your data. When file coordination is required, perform the write inside the documented NSFileCoordinator accessor and then report the actual write or coordination error. Generate from a stable snapshot and support cancellation or failure without leaving a partially written file that looks complete.

On the receiving side, NSFilePromiseReceiver lets a view accept file promises. Its receive operation writes the promised file asynchronously to a destination directory and calls back with the resulting URL or error. Show progress while the promise is outstanding, use a background operation queue, and do not attempt to open the file before the callback succeeds. Apple recommends handling the promise before the file-URL fallback where both are offered, because the promise can represent a higher-quality or not-yet-materialized version.

func acceptDrop(
    _ receiver: NSFilePromiseReceiver,
    in destinationDirectory: URL,
    on queue: OperationQueue,
    completion: @escaping (Result<URL, Error>) -> Void
) {
    receiver.receivePromisedFiles(
        atDestination: destinationDirectory,
        options: [:],
        operationQueue: queue
    ) { fileURL, error in
        if let error {
            completion(.failure(error))
        } else {
            completion(.success(fileURL))
        }
    }
}

The caller must keep the destination directory writable and handle multiple receivers when a drag contains several promised files. If the app’s UI needs the result, marshal the completion back to the main queue after the file is fully received. Validate the downloaded file before importing it, and coordinate subsequent access if the file may be managed by another process or provider.

Drag destination decisions

An NSView or table/collection view should register only the types it can actually consume. During dragging, inspect offered types and current operation, then indicate whether the destination accepts the proposed drop. At the perform phase, read the data or start promise receipt and return a result that reflects whether the operation was accepted. Do not promise a successful import before asynchronous file receipt has completed; update progress and error UI from the completion result.

Internal drags and external drags can represent different operations. An internal row reorder may carry a stable model identifier and move an existing record; an external file drop may copy or import a file. Distinguish them with a private pasteboard type or source identity rather than guessing from URL shape. Make copy, move, and link behavior explicit so a drag from Finder cannot accidentally delete the source file.

Failure handling and verification

Test pasteboard writes with a receiver that asks only for a secondary representation, an owner that is released early, and a user who replaces the clipboard before a delayed read. Exercise Unicode, large data, multiple items, malformed custom payloads, and a type the app advertises but cannot actually serialize. Ensure each failure leaves the existing document state unchanged and produces a recoverable message.

For file promises, test a slow source, a cancelled drop, a read-only destination, a duplicate filename, a disk-full condition, a package/directory promise, and a write error after partial output. Confirm the completion handler is called exactly once on every code path, temporary files are cleaned up, and the UI remains interactive while generation and receipt run. Test both directions: exporting to Finder or another supported app, and importing from Finder, Mail, Safari, Photos, or a controlled test provider.

Instrument only the transfer facts needed to diagnose reliability: offered type identifiers, item count, payload size, duration, promise success/failure category, and sanitized destination result. Avoid logging clipboard contents, filenames containing personal data, or raw custom payloads. Acceptance criteria should require accurate type declarations, stable lazy-provider snapshots, bounded UI work, explicit promise completion, valid receiving behavior, and clean recovery from cancelled or failed transfers.

The pasteboard works best when an application treats it as a negotiated, asynchronous boundary. Represent each item with truthful types, supply expensive data only when requested, use file promises when the bytes do not exist yet, and validate everything on receipt. That model interoperates better with Finder and other apps while avoiding stale selection bugs, UI stalls, and assumptions that clipboard contents remain unchanged.

Related:

Sources:

Comments