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.
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 callself
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 */ }
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 */ }
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.