How to Perfectly Center Selected Text in iOS PDFView (Swift & SwiftUI)
When working with Apple's PDFKit in iOS, navigating to a specific selection is a common requirement. However, developers frequently discover that passing a selection's midpoint to PDFDestination(page:at:) scrolls the selection to the top of the screen rather than centering it in the viewport.
Why Does PDFDestination Scroll to the Top?
The standard behavior of PDFDestination(page:at:) in PDFKit is designed to position the specified coordinate at the top-left (or top edge) of the visible viewport. Furthermore, PDFKit uses the Core Graphics coordinate system (where the origin (0,0) is at the bottom-left corner and y increases upwards), while UIKit uses the top-left coordinate system where y increases downwards.
Because pdfView.go(to:) aligns the target point with the top of the visible screen, creating a destination directly at bounds.midY will place the text right at the top edge of your view.
The Solution: Converting Coordinates to the Underlying UIScrollView
The most reliable and responsive way to center any content in a PDFView is to convert the selection's page coordinates into the coordinate space of the underlying UIScrollView and animate the scroll view's contentOffset.
Step-by-Step Implementation
- Convert the selection's bounding box from page coordinates to the
PDFView's coordinate system usingpdfView.convert(_:from:). - Locate the underlying
UIScrollViewinside thePDFViewhierarchy. - Calculate the target offset needed to place the vertical midpoint of the selection at the center of the scroll view.
- Clamp the offset to prevent overscrolling past the document boundaries and animate the offset change.
Swift Helper Extension
You can create a reusable extension on PDFView to handle centering any PDFSelection or CGRect:
import PDFKit
import UIKit
extension PDFView {
/// Centers the specified PDFSelection in the view
func centerSelection(_ selection: PDFSelection, animated: Bool = true) {
guard let page = selection.pages.first else { return }
let pageBounds = selection.bounds(for: page)
centerRect(pageBounds, on: page, animated: animated)
}
/// Centers a given page coordinate rectangle in the view
func centerRect(_ pageRect: CGRect, on page: PDFPage, animated: Bool = true) {
guard let scrollView = self.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView else {
return
}
// Convert the page rect to PDFView coordinates
let viewRect = self.convert(pageRect, from: page)
// Calculate the centered Y offset
let targetOffsetY = viewRect.midY - (scrollView.bounds.height / 2.0)
// Clamp offset within valid scrollable range
let minOffsetY = -scrollView.adjustedContentInset.top
let maxOffsetY = max(0, scrollView.contentSize.height - scrollView.bounds.height + scrollView.adjustedContentInset.bottom)
let clampedOffsetY = min(max(targetOffsetY, minOffsetY), maxOffsetY)
let targetOffset = CGPoint(x: scrollView.contentOffset.x, y: clampedOffsetY)
scrollView.setContentOffset(targetOffset, animated: animated)
}
}
Full Updated SwiftUI Example
Here is how to integrate this solution directly into your SwiftUI project:
import SwiftUI
import PDFKit
struct ContentView: View {
@State private var pdfView: PDFView? = nil
var body: some View {
VStack {
Button("Center Selection") {
guard let pdfView = pdfView,
let selection = pdfView.currentSelection else {
return
}
pdfView.centerSelection(selection, animated: true)
}
.buttonStyle(.borderedProminent)
.padding()
PDFViewRepresentable(pdfView: $pdfView)
}
}
}
struct PDFViewRepresentable: UIViewRepresentable {
@Binding var pdfView: PDFView?
func makeUIView(context: Context) -> PDFView {
let view = PDFView()
view.autoScales = true
view.displayMode = .singlePageContinuous
view.displayDirection = .vertical
if let url = Bundle.main.url(forResource: "Lorem Ipsum (long)", withExtension: "pdf") {
view.document = PDFDocument(url: url)
}
DispatchQueue.main.async {
self.pdfView = view
}
return view
}
func updateUIView(_ uiView: PDFView, context: Context) {
// Handle state updates if necessary
}
}
Key Takeaways
- Coordinate conversion:
PDFView.convert(_:from:)accounts for dynamic zooming (scaleFactor) and page layout modes (single-page or continuous). - Clamping boundaries: Always clamp the calculated
contentOffsetbetweenminOffsetYandmaxOffsetYto prevent bounce glitches when selections are near the beginning or end of the document. - Animation: Using
UIScrollView.setContentOffset(_:animated:)provides smooth native deceleration curves out of the box without needing externalUIView.animatewrappers.