PSPDFKit 9.5 Migration Guide

This guide covers updating an iOS or Mac Catalyst project from PSPDFKit 9.4 for iOS to PSPDFKit 9.5 for iOS. We encourage you to update as soon as possible, in order to take advantage of future new features and fixes.

PSPDFKit 9.5 for iOS fully supports iOS 12 and 13, and it includes preliminary compatibility with iOS 14. Xcode 11.5 or later is required to use this version of the SDK. Learn more in our Version Support guide.

Handling User Interactions

With PSPDFKit 9.5 for iOS, the way user interactions are handled in PDFViewController has been completely revamped to be more powerful and customizable than ever. Under the new interactions property, you’ll find a selection of interaction components, all of which are responsible for handling a single user interaction.

  • selectAnnotation — Responsible for selecting annotations.

  • deselectAnnotation — Responsible for discarding the current annotation selection.

  • transformAnnotation — Responsible for moving, resizing, and adjusting selected annotations.

  • editAnnotation — Responsible for initiating the editing of form elements, free text annotations, and link annotations.

  • openLinkAnnotation — Responsible for “opening” link annotations, i.e. executing their actions.

  • selectText — Responsible for initiating and changing text selection.

  • deselectText — Responsible for discarding the current text selection.

Miscellaneous Interaction Components

  • fastScroll — Responsible for changing pages by tapping near the edge of the document.

  • smartZoom — Responsible for zooming in on and out of content on a page.

  • toggleUserInterface — Responsible for toggling the user interface.

Interaction Components Composed of Other Components

There are also three interaction components that are composed of other interaction components. You can use them to conveniently customize several interaction components at once:

Customizing Conditions of User Interactions

You can take advantage of activation conditions to allow or disallow certain interaction components to proceed. Activation condition closures have three parameters: a context object that provides information about a user interaction; a point at which it is taking place; and a coordinateSpace in which the aforementioned point is provided. For example, to disallow selecting annotations on a certain page, you can write the following:

interactions.selectAnnotation.addActivationCondition { context, point, coordinateSpace in
    return context.pageView.pageIndex != 0
}

ℹ️ Note: Some interaction components, like toggleUserInterface, have no additional information they can provide except a point and a coordinateSpace. Therefore, the type of the context parameter of their activation condition and callback closures will be NSNull.

If you want to completely disable an interaction component, instead of adding an activation condition that returns false, you can use its isEnabled property. For example, the following line will disable all user interactions related to annotations:

interactions.allAnnotationInteractions.isEnabled = false

Responding to User Interactions

If you want to respond to a certain user interaction without modifying its behavior, you can take advantage of activation callbacks. Activation callback closures have the exact same parameters as activation condition closures and are executed just before a user interaction takes place. For example, to print a console message every time a smart zoom is performed, you can write the following:

interactions.selectAnnotation.addActivationCallback { context, point, coordinateSpace in
    print("Will smart zoom to rect: \(context.targetRect) in scroll view: \(context.scrollView).")
}

❗ Important: Make sure you’re not creating any strong reference cycles when using activation conditions and callbacks. If you’re adding them in a PDFViewController subclass and you need to call self from within, make sure to capture it weakly using [weak self]. For more information about strong reference cycles in closures, check out the Automatic Reference Counting article.

Using Your Own Gesture Recognizers

The new user interaction handling mechanism introduced in PSPDFKit 9.5 for iOS was designed to work seamlessly with your own gesture recognizers. The interaction components provide a set of functions you can use to set up both failure and simultaneous recognition relationships between it and your own gesture recognizers.

For example, to add a long-press gesture recognizer that takes priority over selecting text but should only begin if selecting an annotation failed, you can write the following:

// Create your gesture recognizer.
let gestureRecognizer = UILongPressGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(longPressGestureRecognizerDidChangeState))
gestureRecognizer.delegate = self

// Set up the failure requirements.
gestureRecognizer.require(toFail: interactions.selectAnnotation)
interactions.selectText.require(toFail: gestureRecognizer)

// Add your gesture recognizer to the document view controller's view.
documentViewController?.view.addGestureRecognizer(gestureRecognizer)

// Decide whether your gesture recognizer should begin.
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    return /* condition */
}

// Perform your action.
@objc func longPressGestureRecognizerDidChangeState(_ gestureRecognizer: UIGestureRecognizer) {
    /* action */
}

❗ Important: Using the gestureRecognizerShouldBegin(_:) delegate method is crucial if you’re setting up requirements between the built-in interaction components and your own gesture recognizer. If you don’t implement it, your gesture recognizer will never fail, fulfilling the failure requirement and effectively blocking the interaction component that depends on it from ever proceeding. To learn more, check out the articles on coordinating multiple gesture recognizers and the gesture recognizer state machine.

You can also have your own gesture recognizer work simultaneously with the built-in interaction components. For example, to add a double-tap gesture recognizer that doesn’t block smart zoom, you can write the following:

// Create your own gesture recognizer.
let gestureRecognizer = UITapGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(doubleTapGestureRecognizerDidChangeState))
gestureRecognizer.numberOfTapsRequired = 2

// Set up the simultaneous recognition relationship with smart zoom.
interactions.smartZoom.allowSimultaneousRecognition(with: gestureRecognizer)

// Add your gesture recognizer to the document view controller's view.
documentViewController?.view.addGestureRecognizer(gestureRecognizer)

// Perform your action.
@objc func doubleTapGestureRecognizerDidChangeState(_ gestureRecognizer: UIGestureRecognizer) {
    /* action */
}

For more information on how to use the interaction component activation conditions, callbacks, and your own gesture recognizers to achieve specific outcomes, check out the migration cases below.

Migration Cases for Handling User Interactions

Observing Tap and Long-Press Gestures

If you previously used pdfViewController(_:didTapOn:at:), singleTapped(_:), or singleTapped(at:) to observe tap gestures, you must now use your own UITapGestureRecognizer and set up its relationship with the built-in interaction components:

// Before you would have...
func pdfViewController(_ pdfController: PDFViewController, didTapOn pageView: PSPDFPageView, at viewPoint: CGPoint) -> Bool {
    print("Tapped at point: \(viewPoint) in page view: \(pageView)")
    return false
}
// Now, set up your gesture recognizer.
let gestureRecognizer = UITapGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(tapGestureRecognizerDidChangeState))

// Make it work simultaneously with all built-in interaction components.
interactions.allInteractions.allowSimultaneousRecognition(with: gestureRecognizer)

// Add your gesture recognizer to the document view controller's view.
documentViewController?.view.addGestureRecognizer(gestureRecognizer)

// Perform your action.
@objc func tapGestureRecognizerDidChangeState(_ gestureRecognizer: UITapGestureRecognizer) {
    if gestureRecognizer.state == .ended, let documentViewController = documentViewController {
        if let pageView = documentViewController.visiblePageView(at: gestureRecognizer.location(in: documentViewController.view)) {
            print("Tapped at point: \(gestureRecognizer.location(in: pageView)) in page view: \(pageView)")
        }
    }
}

Similarly, if you previously used pdfViewController(_:didLongPressOn:at:gestureRecognizer) or longPress(_:) to observe long-press gestures, you must now use your own UILongPressGestureRecognizer:

// Create your own gesture recognizer.
let gestureRecognizer = UILongPressGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(longPressGestureRecognizerDidChangeState))

// Make the gesture recognizer work simultaneously with all interactions.
interactions.allInteractions.allowSimultaneousRecognition(with: gestureRecognizer)

// Add your gesture recognizer to the document view controller's view.
documentViewController?.view.addGestureRecognizer(gestureRecognizer)

// Perform your action.
@objc func longPressGestureRecognizerDidChangeState(_ gestureRecognizer: UILongPressGestureRecognizer) {
    if gestureRecognizer.state == .began {
        print("Long press began at point: \(gestureRecognizer.location(in: view))")
    } else if gestureRecognizer.state == .ended {
        print("Long press ended at point: \(gestureRecognizer.location(in: view))")
    }
}

If you used the above methods and relied on their return value to conditionally prevent default gesture handling from proceeding, you must set up a failure relationship with all built-in interaction components instead:

// Before you would have...
func pdfViewController(_ pdfController: PDFViewController, didTapOn pageView: PSPDFPageView, at viewPoint: CGPoint) -> Bool {
    guard /* condition */ else {
        return true
    }
    /* action */
    return false
}
// Now, create your own gesture recognizer.
let gestureRecognizer = UITapGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(tapGestureRecognizerDidChangeState))
gestureRecognizer.delegate = self

// Make it work simultaneously with all built-in interaction components.
interactions.allInteractions.require(toFail: gestureRecognizer)

// Add your gesture recognizer to the document view controller's view.
documentViewController?.view.addGestureRecognizer(gestureRecognizer)

// Move the condition out to the delegate method.
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    return /* reverse condition */
}

// Perform your action.
@objc func tapGestureRecognizerDidChangeState(_ gestureRecognizer: UITapGestureRecognizer) {
    if gestureRecognizer.state == .ended {
        /* action */
    }
}

ℹ️ Note: Both the gestureRecognizerShouldBegin(_:) delegate method and interaction component activation conditions require a true return value to proceed. If you previously used the return value of true to disable the default touch handling, remember to invert that condition now.

Customizing Tappable Annotations

If you previously used tappableAnnotations(at:) or tappableAnnotationsForLongPress(at:) to customize which annotations can be selected, you can now take advantage of interaction component activation conditions to do it:

// Before you would have...
override func tappableAnnotations(at viewPoint: CGPoint) -> [Annotation] {
    super.tappableAnnotations(at: viewPoint).filter { annotation in
        !(annotation is InkAnnotation)
    }
}
// Now, add an activation condition to the interaction component responsible for selecting annotations.
interactions.selectAnnotation.addActivationCondition { context, point, coordinateSpace in
    !(context.annotation is InkAnnotation)
}

// If you want to disable editing certain annotations, you can also add a condition to the `editAnnotation` interaction component.
interactions.editAnnotation.addActivationCondition { context, point, coordinateSpace in
    !(context.annotation is LinkAnnotation)
}

ℹ️ Note: The distinction between “tappable annotations” and “tappable annotations for long-press” is gone in PSPDFKit 9.5 for iOS.

Using a Double-Tap Gesture to Select Annotations

You can use your own gesture recognizers to replace some of the built-in interaction components. For example, to use a double-tap (instead of a single-tap) gesture to select annotations, you can write the following:

// Disable the interaction component responsible for selecting annotations.
interactions.selectAnnotation.isEnabled = false

// Set up your own double-tap gesture recognizer.
let gestureRecognizer = UITapGestureRecognizer()
gestureRecognizer.addTarget(self, action: #selector(doubleTapGestureRecognizerDidChangeState))
gestureRecognizer.numberOfTapsRequired = 2
gestureRecognizer.delegate = self

// Set up a failure relationship with text selection and smart zoom, since they can also recognize double-tap gestures.
interactions.selectText.require(toFail: gestureRecognizer)
interactions.smartZoom.require(toFail: gestureRecognizer)

// Add your gesture recognizer to the document view controller's view.
documentViewController?.view.addGestureRecognizer(gestureRecognizer)

// Allow your gesture recognizer to proceed only if there is a selectable annotation.
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    interactions.selectAnnotation.canActivate(at: gestureRecognizer.location(in: view), in: view)
}

// Perform annotation selection once the gesture is recognized.
@objc func doubleTapGestureRecognizerDidChangeState(_ gestureRecognizer: UIGestureRecognizer) {
    if gestureRecognizer.state == .ended {
        interactions.tryToSelectAnnotation(at: gestureRecognizer.location(in: view), in: view)
    }
}

You can apply the same thought process to use different gestures for other built-in interaction components.

Further Reading

For more information about the new user interaction handling mechanism, check out the documentation of the PSPDFDocumentViewInteractions protocol and the PSPDFInteractionComponent class.

API Changes