Blog Post

Replicating the iOS Text Magnifying Glass

Illustration: Replicating the iOS Text Magnifying Glass

Since iOS 15, Apple’s mobile operating system has offered a new magnifying glass that hovers over text and allows for more accurate text selection. At PSPDFKit, we’re always trying to stay on top of latest improvements, and we try to create a native experience on all platforms. That’s why we decided to also bring the new text magnifying glass to our user interface (UI). Since we don’t use standard text UI elements to render page text, we needed to replicate the system UI and behavior as closely as possible to make it look seamless to users familiar with the default system behavior. In this post, we’ll go into details on how we achieved a magnifying glass resembling the system default one, with some improvements sprinkled on top.

History

The iOS magnifying glass was shown when selecting text up until iOS 13, when it was removed from the system in favor of more fine-tuned selection gestures. Starting with iOS 15, the system again shows a magnifying glass when selecting text. This includes a refreshed design and some new behavior, like hiding the magnifying glass when the selection moves downward. In some cases, this can lead to the magnifying glass becoming hidden until a new gesture is performed (which arguably could also be seen as a problem instead of a feature).

Features

The system magnifying glass in iOS 15 is capsule shaped — rectangular with rounded corners — and it’s displayed on iPhone and iPad when a user begins selecting text and during text selection. The magnifying glass is located above the touch location, and it shows the content underneath it, which is usually text. This allows for more precise selection.

Mimicking the System UI

Since PSPDFKit should feel right at home on each platform, we decided to follow this change and also show a magnifying glass on iOS 15, with only slight tweaks to make selection feel seamless in documents.

To create a UI that resembles this, we needed to recreate the UI and add a way to render the document content in the magnifying glass.

To render the content, we use the snapshotView(afterScreenUpdates:) API. Using render(in:) or drawHierarchy(in:afterScreenUpdates:) would’ve also been a possibility, but since we don’t need to apply any graphical effects on the captured view, the former is preferred, as it’s much faster at snapshotting.

We also want to display the magnifying glass when selecting downward — which the system doesn’t do — so we made this a conscious behavior change.

Implementation

To show a magnifying glass on top of other content, we first created a separate UIWindow containing only the magnifying glass UI. That window is, in turn, in a window scene that the magnifying glass appears in when in use. It shows a blank view controller at first, with user interactions disabled:

let window = UIWindow()
window.windowScene = windowScene
window.frame = windowScene.coordinateSpace.bounds
window.isUserInteractionEnabled = false
window.rootViewController = UIViewController()
window.backgroundColor = nil
window.isHidden = false

Once the magnifying glass is visible, we add a custom LoupeView to the window. This consists of various layers that mimic the system UI, as well as a layer called PeekLayer that handles showing the content (which we’ll get to later). It has a constant size, so all we need to do is change its center and update the content shown inside of it.

The loupe view has a method called show(for:centeringAt:), which is the main entry point for showing the magnifying glass. This method takes a view that should be shown in the magnifying glass — in our case, the page view that renders a document’s content. Additionally, it takes a CGPoint to specify the actual point that should be shown inside the magnifying glass, and it centers around this point. This method looks something like this:

func show(for view: UIView, centeringAt point: CGPoint) {
    center = superview.convert(point, from: view)
    peekLayer.target = (view, point)
    peekLayer.updateSnapshotLayer()
}

We set the center of the loupe view to update its location in the window, and we set the target of the PeekLayer to the passed-in view and point. Then, we trigger an update of the snapshot.

The peek layer does the heavy lifting of providing an accurate representation of the content. It operates with the target to update its snapshotLayer, which is the content we want to actually show in the magnifying glass.

In the updateSnapshotLayer() method, the snapshot of the content view is taken via snapshotView(afterScreenUpdates:) and used in code as the snapshotLayer so that we can show the content in the magnifying glass. And the frame of the snapshot is centered around the point provided, which is usually the touch location during text selection:

func updateSnapshotLayer() {
	// Make sure we have a view to snapshot.
    guard let target = target, let snapshotView = target.view.snapshotView(afterScreenUpdates: false) else {
        snapshotLayer = nil
        return
    }
    // Reposition the snapshot layer.
    snapshotView.layer.frame = CGRect(
        origin: CGPoint(x: -(target.point.x - bounds.width / 2), y: -(target.point.y - bounds.height / 2)),
        size: snapshotView.bounds.size
    )
    // Insert the snapshot layer into the view hierarchy.
    snapshotLayer = snapshotView.layer
}

To match the system even more closely, we also added some animations to move the magnifying glass upward when it appears and a bit downward once it hides.

The video below shows what the replicated magnifying glass (on the left side) looks like in comparison to the system magnifying glass in Safari (on the right) on iOS:

Conclusion

In this post, we went over the crucial details of how we reimplemented the system magnifying glass. Since PSPDFKit 11.2 for iOS, we’ve been shipping the new text magnifying glass when starting text selection, changing a selection, or marking up text in a document. If you’re curious how this looks in practice, download our free PDF Viewer app.

Author
Stefan Kieleithner iOS Engineer

Stefan began his journey into iOS development in 2013 and has been passionate about it ever since. In his free time, he enjoys playing board and video games, spending time with his cats, and gardening on his balcony.

Related products
Share post
Free trial Ready to get started?
Free trial