Fix: PDFView Selected Text Centers at Top Instead of Screen Center in SwiftUI

Fix: PDFView Selected Text Centers at Top Instead of Screen Center in SwiftUI

Written by

in

Quick answer: PDFView.go(to:) centers the destination point at the top of the visible area, not the screen center. Fix it by accessing the internal UIScrollView, converting the selection bounds to view coordinates, and setting the content offset so the selection’s vertical midpoint aligns with the view’s vertical midpoint.

Why this happens

PDFView.go(to:) positions the given PDFDestination point at the top of the visible viewport. When you create a destination at the selection’s geometric center (bounds.midX, bounds.midY), that point lands at the top edge of the screen, so the selection appears centered horizontally but stuck to the top vertically.

The root cause is a coordinate-space mismatch. selection.bounds(for: page) returns coordinates in page space (origin at bottom-left of the page). PDFDestination also expects page-space coordinates. However, the viewport center you actually want is expressed in view space (origin at top-left of the PDFView). Simply halving the page height (page.bounds(for: .bleedBox).height / 2) doesn’t help because that value is still in page space and doesn’t account for zoom, scroll position, or the fact that the visible area may be smaller than a full page.

How to fix it

1. Use the underlying UIScrollView (recommended)

PDFView is a UIView subclass that embeds a UIScrollView. By scrolling that scroll view directly, you work in view coordinates where the screen center is simply pdfView.bounds.midY. This approach handles zoom, page gaps, and partial-page visibility automatically.

First, add a helper to find the scroll view:

extension UIView {
    var pdfScrollView: UIScrollView? {
        if let scrollView = self as? UIScrollView { return scrollView }
        for subview in subviews {
            if let scrollView = subview.pdfScrollView { return scrollView }
        }
        return nil
    }
}

Then implement the button action:

Button("Center Selection") {
    guard let pdfView = self.pdfView,
          let selection = pdfView.currentSelection,
          let page = selection.pages.first,
          let scrollView = pdfView.pdfScrollView else { return }

    // 1. Selection bounds in page coordinates
    let bounds = selection.bounds(for: page)

    // 2. Convert to PDFView (view) coordinates
    let selectionInView = pdfView.convert(bounds, from: page)

    // 3. Desired scroll offset so selection's midY lands at view's midY
    let targetOffsetY = scrollView.contentOffset.y + (selectionInView.midY - pdfView.bounds.midY)

    // 4. Clamp to scrollable range
    let maxOffsetY = max(0, scrollView.contentSize.height - scrollView.bounds.height)
    let clampedOffsetY = max(0, min(targetOffsetY, maxOffsetY))

    // 5. Animate
    scrollView.setContentOffset(
        CGPoint(x: scrollView.contentOffset.x, y: clampedOffsetY),
        animated: true
    )
}

Why this works:
– pdfView.convert(bounds, from: page) translates the selection rectangle from page space to the PDFView‘s coordinate system.
– selectionInView.midY - pdfView.bounds.midY is the vertical distance between the selection center and the view center.
– Adding that distance to the current contentOffset.y moves the scroll view exactly that amount.
– Clamping prevents scrolling past the document’s top or bottom.

2. PDFDestination with coordinate conversion (legacy)

The older approach adjusts the destination point passed to go(to:) so that, after PDFView’s internal logic places that point at the top, the selection ends up centered. It requires converting the view’s center point into page space and offsetting the selection’s midpoint by that delta.

Button("Center Selection") {
    guard let pdfView = self.pdfView,
          let selection = pdfView.currentSelection,
          let page = selection.pages.first else { return }

    let bounds = selection.bounds(for: page)

    // View's top-left and center expressed in page coordinates
    let topLeftPage = pdfView.convert(CGPoint(x: 0, y: 0), to: page)
    let centerPage  = pdfView.convert(CGPoint(x: pdfView.bounds.midX, y: pdfView.bounds.midY), to: page)

    let offsetX = topLeftPage.x - centerPage.x
    let offsetY = topLeftPage.y - centerPage.y

    let destinationPoint = CGPoint(
        x: bounds.midX + offsetX,
        y: bounds.midY + offsetY
    )

    let destination = PDFDestination(page: page, at: destinationPoint)
    UIView.animate(withDuration: 0.3) {
        pdfView.go(to: destination)
    }
}

Drawbacks: This method can misbehave near page boundaries, when zoomed, or in singlePage display mode because go(to:) still anchors the destination at the viewport top. The scroll-view method is more robust.

How to prevent it

  • Always convert geometry between coordinate spaces (page ↔ pdfView ↔ scrollView) explicitly; never assume they align.
  • Prefer manipulating the embedded UIScrollView for viewport-relative positioning—it’s the single source of truth for what the user sees.
  • Clamp scroll offsets to contentSize - bounds to avoid bouncing past document edges.
  • Test with multi-page selections, zoomed content, and different displayMode settings (.singlePage, .singlePageContinuous, .twoUp).

FAQ

Why does PDFDestination put my point at the top of the screen?

PDFView.go(to:) is designed to navigate to a location, making that location the first thing visible. It does not center the point; it aligns it with the top edge of the viewport.

Can I use PDFView.go(to:) with a custom PDFDestination that centers correctly?

Not reliably. The destination point is always placed at the viewport top. You would need to calculate a point above the selection so that the selection falls in the center, but that math breaks when zoom or page gaps change. The scroll-view approach avoids this entirely.

Does this work on macOS with PDFView?

No. On macOS PDFView does not use a UIScrollView; it uses an NSScrollView with different APIs. The coordinate-conversion logic is similar, but you would call scrollPoint(_:) on the NSScrollView‘s documentView instead.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.