Skip to content
macOSDeep Dive Published Updated 7 min readViews unavailable

AppKit Printing on macOS: NSPrintOperation, Page Geometry, and Pagination

Create reliable AppKit print output by separating print settings, view pagination, page coordinate mapping, print-panel policy, and PDF regression tests.

AppKit printing is a rendering operation with a page model, not merely a screenshot sent to a printer. NSPrintOperation coordinates an NSView that draws content and an NSPrintInfo object that describes paper, margins, scaling, and other settings. The view’s print path must define how document content maps into page rectangles, and the operation decides how the output is presented or delivered.

Start by deciding whether you are printing a view, exporting a PDF, or generating a custom document representation. These workflows can share drawing code, but their output requirements differ. A view that is correct on screen may clip when printed because the page size, printable area, scale, and coordinate origin are different.

Configure print information before creating the operation

NSPrintOperation copies most NSPrintInfo values passed to its factory or initializer. Configure print settings first, then create the operation. Mutating the original NSPrintInfo after constructing the operation may not change the operation’s retained settings. Keep the configured print info associated with one operation so tests can reproduce the exact geometry.

import AppKit

@MainActor
func printView(_ view: NSView) -> Bool {
    let info = NSPrintInfo.shared.copy() as! NSPrintInfo
    info.horizontalPagination = .automatic
    info.verticalPagination = .automatic

    let operation = NSPrintOperation(view: view, printInfo: info)
    operation.jobTitle = "Quarterly report"
    operation.showsPrintPanel = true
    operation.showsProgressPanel = true
    return operation.run()
}

The example uses automatic pagination and presents AppKit’s print UI. A headless export path should create the corresponding PDF operation and send output to a controlled destination instead of unexpectedly displaying a panel. Do not force-cast shared print information in reusable production code without handling the possibility of an unexpected object; the code here keeps the sample compact and assumes the normal AppKit shared object.

Printing settings can be influenced by the selected printer and user panel. Use the final operation’s printInfo as the source of truth for layout after the print panel, not a stale pre-panel copy. Avoid hard-coding an A4 or Letter page rectangle without checking the configured paper size and imageable area.

Automatic pagination versus custom page boundaries

For many views, AppKit’s automatic pagination is sufficient. The framework asks the view how to divide its content and applies page adjustment behavior to avoid splitting small elements where possible. If the content has semantic page boundaries, such as a report with headers, footers, chapter breaks, or a table of contents, implement the view’s pagination methods deliberately.

When a view returns true from knowsPageRange(_:), it supplies a one-based page range. AppKit then asks rectForPage(_:) for the view-space rectangle to print for each page. The rectangle must be within the view’s bounds and should correspond to the same immutable document layout used to compute the page count. A page range based on one text revision and page rectangles based on a newer revision can duplicate or omit content.

import AppKit

final class ReportPrintView: NSView {
    private let pageHeight: CGFloat = 720
    private let pageCount: Int

    init(frame: NSRect, pageCount: Int) {
        self.pageCount = max(0, pageCount)
        super.init(frame: frame)
    }

    required init?(coder: NSCoder) { nil }

    override func knowsPageRange(_ range: NSRangePointer) -> Bool {
        guard pageCount > 0 else { return false }
        range.pointee = NSRange(location: 1, length: pageCount)
        return true
    }

    override func rectForPage(_ page: Int) -> NSRect {
        guard page > 0, page <= pageCount else { return .zero }
        return NSRect(x: 0, y: CGFloat(page - 1) * pageHeight,
                      width: bounds.width, height: pageHeight)
    }
}

The constant page height is illustrative, not a universal printable dimension. A real view should calculate page rectangles from the operation’s paper and imageable geometry, margins, scale, and any product-specific page layout. Keep the computed page map stable for the duration of the operation. If the source document changes while printing, either print the captured immutable revision or cancel and restart explicitly.

View coordinates and physical page geometry

The view’s bounds describe its drawing space. The physical page’s paper rectangle and imageable area describe printer output space. Margins, orientation, scaling, and printable bounds affect how the content is placed. Do not assume the origin of a page rectangle is the top-left corner of the paper or that a screen point maps directly to a printed point.

Treat page layout as a deterministic transform from document coordinates into the print operation’s context. Build a layout snapshot that includes effective paper size, orientation, margins, scaling mode, content revision, and page rectangles. Use the same snapshot for pagination and drawing. Avoid reaching into NSPrintOperation.current from arbitrary code; pass the operation or print context to the component that needs it.

Text-heavy output may need a distinct print layout from the screen layout. A narrow on-screen editor may use one column, while print output uses page-width wrapping and headers. Recompute line breaks against the print container, preserve paragraph and keep-with-next semantics where supported, and ensure pagination advances. The Core Text and AppKit text systems can draw text into print contexts, but your document layer must decide which content belongs on each page.

Draw only the requested page

AppKit renders each page from the relevant portion of the view. draw(_:) should respect the dirty rectangle and render only model items intersecting the current page. Avoid expensive network or disk reads in the print drawing callback. Prepare the immutable document snapshot and resolve required images before beginning the print operation.

A page renderer should not mutate the source model. Printing can ask for pages in an order influenced by the print operation, and PDF generation may invoke drawing without an interactive window. A deterministic renderer makes PDF output testable and prevents print-only side effects. If custom drawing uses display-specific colors or sizes, ensure it uses print-appropriate color and resolution behavior instead of reusing stale screen caches.

Headers, footers, page numbers, and crop marks need explicit coordinate placement. Page numbering should use the one-based print page number and a user-visible convention that may include a document offset. Do not derive a printed page label from currentPage unless the operation’s current page is the intended source and has been tested for reverse-order printing.

Printing PDFs and print-panel lifecycle

NSPrintOperation can create PDF or EPS output from a view as well as drive a print job. For PDF output, write to a staging URL or data buffer, check operation success, and only then replace the destination file. Do not assume that creating an operation means it ran successfully; check the return value and any relevant error/reporting path.

The print panel can alter page settings. If the user cancels the panel, the operation should not be treated as a completed print. Test both panel-present and panel-suppressed flows, and make the panel policy clear in the command that starts the operation. Avoid presenting multiple simultaneous print operations from the same document unless the app has a deliberate concurrency policy.

Failure modes and regression testing

Common defects include a final blank page caused by rounding, clipped footer text, split rows, wrong orientation, duplicate first pages, page numbers that start at zero, and output scaled twice. Test page counts at boundaries: content that fits exactly, content that exceeds by one line, an empty document, a very tall image, a long table row, and a document containing mixed fonts and attachments.

Create PDF fixtures from the same print operation used in production and inspect page count, media boxes, text extraction, and rasterized snapshots. Visual diffs catch clipped geometry, while structural assertions catch blank or missing pages. Compare output at multiple paper sizes and margins; a successful Letter render does not prove the layout adapts to A4 or a user-selected custom paper size.

Log document revision, print settings, page count, operation result, and render duration. Do not log full document content. When a print job fails, distinguish panel cancellation, view layout failure, PDF destination error, and printer/driver error instead of reporting one generic “print failed” message.

Acceptance criteria

For a fixed content revision and fixed print settings, page count and page geometry should be deterministic. Every source item should appear exactly once unless the document’s design explicitly repeats it. No page rectangle should be empty unexpectedly, and all rendered content should lie within the intended printable region. Confirm that printing does not change document dirty state and that a canceled panel does not mark the operation complete.

Treat printing as a separate layout target with explicit settings, page ownership, and a reproducible renderer. NSPrintOperation orchestrates output; it cannot infer your document’s pagination rules or validate that the final page is complete.

Related:

Sources:

Comments