Customize User Interactions on iOS

This guide discusses how PSPDFKit handles user interactions and presents some examples of how this mechanism can be customized to achieve the desired behavior.

Overview of User Interaction Components

The starting point of working with user interactions is the interactions property of PDFViewController, which contains a set of user interaction components. These are objects responsible for handling single or multiple user interactions. In addition, they can be customized and observed.

Below is a table showing each component and what it’s responsible for.

Component Responsible for…
selectAnnotation Selecting annotations on a page.
deselectAnnotation Discarding the current annotation selection.
transformAnnotation Moving, resizing, and adjusting the currently selected annotations.
editAnnotation Initiating the editing of form elements, free text annotations, and link annotations.
openLinkAnnotation “Opening” link annotations, i.e. executing their actions.
selectText Selecting text on a page and changing the current text selection.
deselectText Discarding the current text selection.
fastScroll Changing pages by tapping near the edges of a document.
smartZoom Zooming in on and out of content on a page.
toggleUserInterface Toggling the user interface.

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

Component Composed of…
allAnnotationInteractions All components related to annotations.
allTextInteractions All components related to text selection.
allInteractions All components.

Customizing the User Interaction Components

By default, all user interaction components are enabled. To disable one, you can use its isEnabled property. For example, the following line will disable all user interaction components related to annotations:

interactions.allAnnotationInteractions.isEnabled = false
interactions.allAnnotationInteractions.enabled = NO;

For more advanced use cases, you can take advantage of activation conditions to allow or disallow certain user interaction components to proceed only if a particular requirement is met. 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
}
[interactions.selectAnnotation addActivationCondition:^BOOL(PSPDFAnnotationSelectionContext *context, CGPoint point, id<UICoordinateSpace> coordinateSpace) {
    return context.pageView.pageIndex != 0;
}];

Some user interaction components, like toggleUserInterface, have no additional information they can provide except a point and a coordinateSpace. Therefore, the type of their context will be NSNull.

You can add as many activation conditions to a user interaction component as you want. They will be evaluated in the order they were added until one of them returns false. This means they’re not suitable for performing side effects. To learn more about performing side effects when a user interaction occurs, check out the following section.

Responding to User Interaction Components

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.smartZoom.addActivationCallback { context, point, coordinateSpace in
    print("Will smart zoom to: \(context.targetRect) in: \(context.scrollView)")
}
[interactions.smartZoom addActivationCallback:^(PSPDFSmartZoomContext *context, CGPoint point, id<UICoordinateSpace> coordinateSpace) {
    NSLog(@"Will smart zoom to: %@ in: %@", NSStringFromCGRect(context.targetRect), context.scrollView);
}];

It’s not possible to prevent a user interaction from taking place when inside an activation callback closure. To learn about preventing a user interaction from taking place, check out the previous section or use PDFViewControllerDelegate methods related to tapping on annotations.

Information

Make sure you’re not creating any strong reference cycles when using activation conditions and callbacks. If you’re adding them in a subclass of PDFViewController and you need to call self from within, make sure to capture it weakly using [weak self] (or using a __weak variable in Objective-C). For more information about strong reference cycles in closures, check out Apple’s Automatic Reference Counting article.

Working with Custom Gesture Recognizers

User interaction components were designed to work seamlessly with your own gesture recognizers. They provide a set of functions you can use to set up both failure and simultaneous recognition relationships between them 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 longPressGestureRecognizer = UILongPressGestureRecognizer()
longPressGestureRecognizer.addTarget(self, action: #selector(longPressGestureRecognizerDidChangeState))
longPressGestureRecognizer.delegate = self

// Set up the failure requirements with other components.
longPressGestureRecognizer.require(toFail: interactions.selectAnnotation)
interactions.selectText.require(toFail: longPressGestureRecognizer)

// 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 */
}
// Create your gesture recognizer.
UILongPressGestureRecognizer *longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] init];
[longPressGestureRecognizer addTarget:self action:@selector(longPressGestureRecognizerDidChangeState)];
longPressGestureRecognizer.delegate = self;

// Set up the failure requirements with other components.
[longPressGestureRecognizer pspdf_requireGestureRecognizersInComponentToFail:interactions.selectAnnotation];
[interactions.selectAnnotation requireGestureRecognizerToFail:longPressGestureRecognizer];

// Add your gesture recognizer to the document view controller's view.
[documentViewController.view addGestureRecognizer:longPressGestureRecognizer];

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

// Perform your action.
- (void)longPressGestureRecognizerDidChangeState:(UIGestureRecognizer *)gestureRecognizer {
    /* action */
}
Information

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. This, in turn, effectively blocks the interaction component that depends on it from ever proceeding. To learn more, check out Apple’s 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 gesture recognizer.
let doubleTapGestureRecognizer = UITapGestureRecognizer()
doubleTapGestureRecognizer.addTarget(self, action: #selector(doubleTapGestureRecognizerDidChangeState))
doubleTapGestureRecognizer.numberOfTapsRequired = 2

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

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

// Perform your action.
@objc func doubleTapGestureRecognizerDidChangeState(_ gestureRecognizer: UIGestureRecognizer) {
    /* action */
}
// Create your gesture recognizer.
UITapGestureRecognizer *doubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] init];
[doubleTapGestureRecognizer addTarget:self action:@selector(doubleTapGestureRecognizerDidChangeState)];
doubleTapGestureRecognizer.numberOfTapsRequired = 2;

// Set up the simultaneous recognition relationship with the smart zoom component.
[interactions.smartZoom allowSimultaneousRecognitionWithGestureRecognizer:doubleTapGestureRecognizer];

// Add your gesture recognizer to the document view controller's view.
[documentViewController.view addGestureRecognizer:doubleTapGestureRecognizer];

// Perform your action.
- (void)doubleTapGestureRecognizerDidChangeState:(UIGestureRecognizer *)gestureRecognizer {
    /* action */
}
Information

Not all user interaction components are backed by gesture recognizers, and some of them may be backed by gesture recognizers just on one specific version of iOS. Make sure to thoroughly read the documentation of individual components before using them, and keep an eye on our changelog for any changes related to user interactions.

Dynamic Gesture Recognizer Relationships

If your gesture recognizer uses dynamic failure or simultaneous recognition relationship resolution, you can use contains(_:) to check whether or not a received gesture recognizer is managed by PSPDFKit:

// Require failure if the given gesture recognizer is part of any component.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return interactions.allInteractions.contains(otherGestureRecognizer)
}

// Allow simultaneous recognition if the given gesture recognizer is part of a text selection component.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return interactions.allTextInteractions.contains(otherGestureRecognizer)
}
// Require failure if the given gesture recognizer is part of any component.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return [interactions.allInteractions containsGestureRecognizer:otherGestureRecognizer];
}

// Allow simultaneous recognition if the given gesture recognizer is part of a text selection component.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return [interactions.allTextInteractions containsGestureRecognizer:otherGestureRecognizer];
}

Reimplementing Existing User Interactions

The interactions property provides a selection of methods that can be used from within your custom gesture recognizers’ actions and delegate methods to effectively reimplement certain user interactions using your own means.

For example, to use a double-tap gesture recognizer to select annotations, you must first disable the default selectAnnotation user interaction component, then evaluate its activation conditions in the gestureRecognizerShouldBegin(_:) delegate method, and finally, call tryToSelectAnnotation(at:in:) to commit the action:

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

// Disable the default component.
interactions.selectAnnotation.isEnabled = false

// Set up the failure requirements with other components.
interactions.allInteractions.require(toFail: doubleTapGestureRecognizer)

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

// Ask the default component if it can activate at the given location.
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    let view = gestureRecognizer.view!
    let point = gestureRecognizer.location(in: view)
    return interactions.selectAnnotation.canActivate(at: point, in: view)
}

// Try to select an annotation at the given location.
@objc func doubleTapGestureRecognizerDidChangeState(_ gestureRecognizer: UIGestureRecognizer) {
    if gestureRecognizer.state == .ended {
        let view = gestureRecognizer.view!
        let point = gestureRecognizer.location(in: view)
        interactions.tryToSelectAnnotation(at: point, in: view)
    }
}
// Create your gesture recognizer.
UITapGestureRecognizer *doubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] init];
[doubleTapGestureRecognizer addTarget:self action:@selector(doubleTapGestureRecognizerDidChangeState)];
doubleTapGestureRecognizer.delegate = self;
doubleTapGestureRecognizer.numberOfTapsRequired = 2;

// Disable the default component.
interactions.selectAnnotation.enabled = NO;

// Set up the failure requirements with other components.
[interactions.allInteractions requireGestureRecognizerToFail:doubleTapGestureRecognizer];

// Add your gesture recognizer to the document view controller's view.
[documentViewController.view addGestureRecognizer:doubleTapGestureRecognizer];

// Ask the default component if it can activate at the given location.
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    UIView *view = gestureRecognizer.view;
    CGPoint point = [gestureRecognizer locationInView:view];
    return [interactions.selectAnnotation canActivateAtPoint:point inCoordinateSpace:view];
}

// Try to select an annotation at the given location.
- (void)doubleTapGestureRecognizerDidChangeState:(UIGestureRecognizer *)gestureRecognizer {
    if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
        UIView *view = gestureRecognizer.view;
        CGPoint point = [gestureRecognizer locationInView:view];
        [interactions tryToSelectAnnotationAtPoint:point inCoordinateSpace:view];
    }
}

Calling the canActivate(at:in:) method will ignore the user interaction component’s isEnabled property, but it will still evaluate its activation conditions. This can prove incredibly useful in advanced use cases where you might want to reimplement a certain user interaction while retaining the custom activation logic.

Customization Examples

Observing Tap and Long-Presses Gestures

To observe tap or long-press gestures (or any other gesture for that matter), use your own gesture recognizer and set up its simultaneous relationship with other user interaction components:

// Create your tap gesture recognizer.
let tapGestureRecognizer = UITapGestureRecognizer()
tapGestureRecognizer.addTarget(self, action: #selector(tapGestureRecognizerDidChangeState))

// Create your long-press gesture recognizer.
let longPressGestureRecognizer = UILongPressGestureRecognizer()
longPressGestureRecognizer.addTarget(self, action: #selector(longPressGestureRecognizerDidChangeState))

// Set up the failure requirements with other components.
interactions.allInteractions.allowSimultaneousRecognition(with: tapGestureRecognizer)
interactions.allInteractions.allowSimultaneousRecognition(with: longPressGestureRecognizer)

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

// Log the tap gesture.
@objc func tapGestureRecognizerDidChangeState(_ gestureRecognizer: UIGestureRecognizer) {
    if gestureRecognizer.state == .ended {
            let point = gestureRecognizer.location(in: gestureRecognizer.view!)
            print("Tapped at point: \(point)")
        }
    }
}

// Log the long-press gesture.
@objc func longPressGestureRecognizerDidChangeState(_ gestureRecognizer: UIGestureRecognizer) {
    if gestureRecognizer.state == .began {
            let point = gestureRecognizer.location(in: gestureRecognizer.view!)
            print("Long-press began at point: \(point)")
        }
    } else if gestureRecognizer.state == .ended {
            let point = gestureRecognizer.location(in: gestureRecognizer.view!)
            print("Long-press ended at point: \(point)")
        }
    }
}
// Create your tap gesture recognizer.
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] init];
[tapGestureRecognizer addTarget:self action:@selector(tapGestureRecognizerDidChangeState)];

// Create your long-press gesture recognizer.
UITapGestureRecognizer *longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] init];
[longPressGestureRecognizer addTarget:self action:@selector(longPressGestureRecognizerDidChangeState)];

// Set up the failure requirements with other components.
[interactions.allInteractions allowSimultaneousRecognitionWithGestureRecognizer:tapGestureRecognizer];
[interactions.allInteractions allowSimultaneousRecognitionWithGestureRecognizer:longPressGestureRecognizer];

// Add your gesture recognizer to the document view controller's view.
[documentViewController.view addGestureRecognizer:tapGestureRecognizer];
[documentViewController.view addGestureRecognizer:longPressGestureRecognizer];

// Log the tap gesture.
- (void)tapGestureRecognizerDidChangeState:(UIGestureRecognizer *)gestureRecognizer {
    if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
        CGPoint point = [gestureRecognizer locationInView:gestureRecognizer.view];
        NSLog(@"Tapped at point: %@", NSStringFromCGPoint(point));
    }
}

// Log the long-press gesture.
- (void)tapGestureRecognizerDidChangeState:(UIGestureRecognizer *)gestureRecognizer {
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
        CGPoint point = [gestureRecognizer locationInView:gestureRecognizer.view];
        NSLog(@"Long-press began at point: %@", NSStringFromCGPoint(point));
    } else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
        CGPoint point = [gestureRecognizer locationInView:gestureRecognizer.view];
        NSLog(@"Long-press ended at point: %@", NSStringFromCGPoint(point));
    }
}

Excluding Annotations from Being Tappable

There are four user interaction components that are responsible for recognizing touches over annotations: selectAnnotation, transformAnnotation, editAnnotation, and openLinkAnnotation. You can add activation conditions to appropriate user interaction components to exclude certain annotations from participating:

// Prevent ink annotations from being selectable.
interactions.selectAnnotation.addActivationCondition { context, point, coordinateSpace in
    return !(context.annotation is InkAnnotation)
}

// Prevent annotations in a certain PDF rect from being transformable.
interactions.transformAnnotation.addActivationCondition { context, point, coordinateSpace in
    let pdfRect = CGRect(x: 0, y: 0, width: 400, height: 200)
    let pdfPoint = context.pageView.pdfCoordinateSpace.convert(point, from: coordinateSpace)
    return !pdfRect.contains(pdfPoint)
}

// Prevent free text annotations from being editable.
interactions.editAnnotation.addActivationCondition { context, point, coordinateSpace in
    return !(context.annotation is FreeTextAnnotation)
}

// Prevent opening links anywhere except on the first page.
interactions.openLinkAnnotation.addActivationCondition { context, point, coordinateSpace in
    return context.pageView.pageIndex == 0
}
// Prevent ink annotations from being selectable.
[interactions.selectAnnotation addActivationCondition:^BOOL(PSPDFAnnotationSelectionContext *context, CGPoint point, id<UICoordinateSpace> coordinateSpace) {
    return ![context.annotation isKindOfClass:PSPDFInkAnnotation.class];
}];

// Prevent annotations in a certain PDF rect from being transformable.
[interactions.transformAnnotation addActivationCondition:^BOOL(PSPDFAnnotationSelectionContext *context, CGPoint point, id<UICoordinateSpace> coordinateSpace) {
    CGRect pdfRect = CGRectMake(0, 0, 400, 200);
    CGPoint pdfPoint = [context.pageView.pdfCoordinateSpace convertPoint:point fromCoordinateSpace:coordinateSpace];
    return !CGRectContainsPoint(pdfRect, pdfPoint);
}];

// Prevent free text annotations from being editable.
[interactions.editAnnotation addActivationCondition:^BOOL(PSPDFAnnotationSelectionContext *context, CGPoint point, id<UICoordinateSpace> coordinateSpace) {
    return ![context.annotation isKindOfClass:PSPDFFreeTextAnnotation.class];
}];

// Prevent opening links anywhere except on the first page.
[interactions.openLinkAnnotation addActivationCondition:^BOOL(PSPDFAnnotationSelectionContext<PSPDFLinkAnnotation *> *context, CGPoint point, id<UICoordinateSpace> coordinateSpace) {
    return context.pageView.pageIndex == 0;
}];

Further Reading

For more information about the user interaction components API, check out the documentation of the DocumentViewInteractions protocol and the InteractionComponent class.

To learn more about working with multiple gesture recognizers, check out the articles on coordinating multiple gesture recognizers and the gesture recognizer state machine.

If you want to learn more about working with multiple coordinate spaces, check out our coordinate space conversions guide.

The user interaction handling mechanism described in this guide has been available since PSPDFKit 9.5 for iOS. If you’re migrating from version 9.4 or lower, check out the migration guide, which showcases various migration cases in more detail.