Skip to content
macOSDeep Dive Published Updated 7 min readViews unavailable

NSScrollView on macOS: Clip Geometry, Coordinates, and Magnification

Debug AppKit scrolling by separating document and clip-view coordinates, tracking visible rectangles, preserving anchors, and testing magnification.

An NSScrollView shows a portion of a larger document view through an NSClipView. The scroll view coordinates the clip view and scrollers; the clip view changes its bounds origin to reveal a different part of the document. Many apparent scrolling bugs are coordinate-space mistakes: code compares a point in window coordinates with a rectangle in document coordinates, or assumes the visible region is the same as the clip view’s bounds.

Treat the document view’s geometry, clip view’s bounds, scroll view’s frame, and the visible rect as separate values. Convert points and rectangles through AppKit’s coordinate conversion methods instead of adding offsets by hand. This becomes more important when a view is flipped, embedded in a split view, magnified, or clipped by another ancestor.

Understand the view hierarchy

The usual hierarchy is a scroll view containing a clip view, which contains the document view. The document view can be larger than the visible region, and scrolling changes which part of that document view is exposed. NSScrollView.documentVisibleRect reports the part visible through its content view in the document view’s own coordinate system. NSClipView.documentVisibleRect similarly describes the exposed document region, though it does not account for clipping by ancestors above the clip view.

import AppKit

func makeScrollableCanvas() -> NSScrollView {
    let scrollView = NSScrollView(frame: NSRect(x: 0, y: 0, width: 640, height: 420))
    scrollView.hasVerticalScroller = true
    scrollView.hasHorizontalScroller = true
    scrollView.autohidesScrollers = true

    let canvas = NSView(frame: NSRect(x: 0, y: 0, width: 1800, height: 1200))
    scrollView.documentView = canvas
    return scrollView
}

func visibleCanvasRect(in scrollView: NSScrollView) -> NSRect {
    scrollView.documentVisibleRect
}

The example sets a document frame larger than its viewport. In a real canvas, derive document size from model content and update it through layout, not from the screen’s size. If the document becomes smaller than the clip view, the clip view may adjust its document rect; do not assume the original frame is always the effective scrollable extent.

Convert coordinates instead of inventing offsets

Mouse events are delivered in window coordinates. A drawing or hit-testing routine may need document-view coordinates. Use convert(_:from:) or convert(_:to:) on the relevant view to translate the point. Do not subtract the clip view’s bounds origin manually unless you also account for magnification, flipped-coordinate conventions, transforms, and ancestor offsets.

For selection or drag tracking, capture the stable document coordinate at gesture start, then convert subsequent event positions through the same view hierarchy. The scroll view can move during a drag, so an event’s window coordinate can remain steady while the corresponding document coordinate changes. This is expected: the pointer is over a different document location after the content scrolls underneath it.

When computing which rows or tiles need rendering, use the document-visible rect in the document view’s coordinate system. If another superview clips the scroll view, intersect with its visible rect or query the target view’s visibleRect. Avoid using frame as a proxy for what is actually visible; a frame describes a view relative to its superview, not the currently exposed document region.

Scroll position and preserving an anchor

To scroll programmatically, adjust the clip view’s origin with scroll(to:), then ask the scroll view to reflect the scrolled clip view so its scrollers and related state stay synchronized. Prefer scrolling to a model anchor or a known rect rather than writing a pixel offset that becomes stale when content is inserted above it. For document editors, preserve a stable item identifier plus its offset within the viewport across a reload.

The clip view constrains proposed scroll origins to keep the document view within its valid range. Custom clip views can override constraint behavior, but that introduces additional geometry policy. If implementing a custom scroll view, keep the constraint function deterministic and test content smaller than the viewport, zero-sized content, and magnified content.

Do not assume scrolling is always a user gesture. Programmatic updates, autoscroll during drag, magnification, and layout changes can all modify visible geometry. If work depends on scrolling being settled, observe appropriate begin/end notifications or schedule a coalesced update after bounds change. Avoid expensive full-document work on every scroll event; use visible region invalidation or a debounced task.

Magnification changes document geometry

NSScrollView supports magnification when allowsMagnification is enabled, with configurable minimum and maximum magnification. setMagnification(_:centeredAt:) changes the scale around a point. The meaning of that center point is part of the API contract; test it with real content instead of compensating with a guessed translation. magnify(toFit:) can proportionally scale content so a rectangle fits centered in the scroll view.

Do not scale only the document view’s drawing while leaving its hit testing, text layout, and accessibility geometry unscaled. Prefer the scroll view’s magnification model when the whole document should zoom. Text-heavy documents may need a different policy that changes font metrics or layout instead of magnifying rasterized content. Large magnification can make vector drawing more expensive and increase the visible document extent needed to reach the same logical point.

At each magnification transition, re-evaluate visible rect, selection handles, hover affordances, and cached drawing. If an overlay is attached to the scroll view rather than the document view, it may remain fixed while the content zooms. That can be correct for rulers or controls but incorrect for annotations that should track document coordinates.

Scrollers, insets, and background drawing

Overlay scrollers, content insets, border style, and automatic inset adjustments affect available viewport size. Ask the scroll view for contentSize after configuring its scrollers and border rather than assuming frame size equals the document-visible width. On a window resize, recalculate layout from the current content size.

When an NSClipView is used inside an NSScrollView, Apple advises sending messages that control background drawing state to the scroll view directly. Setting drawsBackground on the clip view can cause trails as the document view scrolls. Put backgrounds in the appropriate view layer or content view and invalidate the changed area when drawing custom canvases.

Do not make a document view larger in response to every scroll event without a content policy. Infinite canvases need an explicit coordinate model and growth strategy. For a finite document, compute its bounds from content and constrain scroll position. For virtualized content, maintain a bounded set of visible elements and avoid constructing a view per record in a huge document.

Notifications and efficient updates

NSScrollView provides notifications around live scrolling and magnification. Use begin/end events for interaction-level behavior such as deferring expensive thumbnail work during a gesture. Notifications do not replace observing the model or re-reading the visible rect at the point of use. A layout pass can update geometry between the notification and deferred work.

When live scrolling, update only what needs to follow the viewport. Avoid layout invalidation of every subview for each clip-view bounds change. Use tiled drawing, layer-backed content when appropriate, and caches keyed by scale and content revision. If a cache is used, treat misses as normal and don’t let cache retention pin the entire document model.

Failure matrix

Test a flipped document view, nested scroll views, narrow split-view width, scrollers appearing and disappearing, small and empty content, programmatic scrolling, live resize, drag autoscroll, magnification at limits, and an outer parent that clips the scroll view. Assert that pointer-to-document conversion is correct, selection remains attached to content, and visible-region rendering never omits exposed pixels.

Log document bounds, clip bounds origin, document-visible rect, magnification, and scroll mode when investigating geometry. Avoid logging user document content. Reproduce with a fixed set of window and display sizes; a coordinate bug can appear only after a specific resize or scale transition.

The scroll view is a geometry coordinator, not just a pair of scrollbars. Keep coordinate spaces explicit, query the actual visible rect, and let the clip view own scrolling mechanics. This makes drawing, hit testing, magnification, and accessibility behave consistently as the window and document change.

Related:

Sources:

Comments