Undo and Redo

PSPDFKit for iOS supports undo and redo operations for creating and editing annotations, including form filling. Users can undo and redo changes using the buttons in AnnotationToolbar.

To achieve this, the SDK uses a standard component named NSUndoManager. If you have never used it before, we recommend studying both the Apple Documentation and the NSHipster article about this topic to understand the principles of how this class works.

PSPDFKit uses an UndoController object that wraps and manages the undo manager and allows more convenient access to properties such as canUndo.

The undo controller also extends Apple’s concept by adding a coalescing mode, which merges changes happening in quick succession into a single undo event. This is used in various places — for example, when a color is chosen from the color picker, we don’t save every single change. Rather we commit the change after a short period of time to provide a better undo experience for the user. This implies that whenever the UndoController object exposes properties, you should use them, and only in cases where there’s no other way to go down to the wrapped undo manager instance.

The UndoController instance is exposed via the undoController property on Document. Undo can also be disabled on a per-document level via the isUndoEnabled property.

Listening for Undo and Redo Changes

If you’re implementing your own undo and redo buttons and are trying to update the enabled state, you need to listen for the following notifications:

For convenience reasons, we also expose a delegate method on AnnotationStateManager that listens to the set above and calls back with the undo- and redo-enabled states:

Copy
1
optional public func annotationStateManager(_ manager: AnnotationStateManager, didChangeUndoState undoEnabled: Bool, redoState redoEnabled: Bool)
1
- (void)annotationStateManager:(PSPDFAnnotationStateManager *)manager didChangeUndoState:(BOOL)undoEnabled redoState:(BOOL)redoEnabled;

Note that this class supports multiple delegates. Use add(_:) and remove(_:) to register them.

See PSCVerticalAnnotationToolbarExample.m for a sample implementation of custom undo and redo buttons.

Shake to Undo

PSPDFKit is a good iOS citizen and supports the native ways of undoing, which on the iPhone is Shake to Undo — see the Undo and Redo section in the iOS Human Interface Guidelines for more on this. Shake to Undo can be deactivated by users in the phone Accessibility settings or via the applicationSupportsShakeToEdit property on UIApplication.

Implementation Notes

Saving annotations will clear the undo and redo stacks. As a result, writing objects into the PDF changes various details, so providing a reliable undo is difficult. Saving happens at various points, including when the application enters the background.

On a technical level, our undo works via KVO and registering each annotation object and tracking changes on a property basis.

Undo/Redo Grouping

By default, each annotation change is recorded as a single undo action. If you’re doing multiple things but you want them to be undone by a single undo action, you can group those changes. This can be done with UndoController.performAsGroup(undoController:closure:name:), which lets you decide what to include in a single undo action. You can group any changes to annotations, like changing their size and color in a single action:

Copy
1
2
3
4
5
UndoController.performAsGroup(undoController: document.undoController, closure: {
  // This is an example of how to group actions for an ink annotation.
  annotation.color = .red
  annotation.lineWidth = 5
}, nil)
Copy
1
2
3
4
5
PSPDFPerformBlockAsGroup(document.undoController, ^{
  // This is an example of how to group actions for an ink annotation.
  annotation.color = UIColor.redColor;
  annotation.lineWidth = 5.0;
}, nil);

Similarly, annotation changes can be made without them being observed for the undo/redo operations. This can be useful when you are triggering a change to an annotation as a result of an action that is not triggered by your user. Or perhaps you are adding temporary annotations for representation purposes and you do not want them to be removed when your user performs the undo action. Such changes can be made using UndoController.performWithoutUndo(undoController:closure:), which ensures none of the changes performed in the closure are observed for the undo/redo operations:

Copy
1
2
3
4
5
6
UndoController.performWithoutUndo(undoController: document.undoController) {
  // This is an example of how to group actions for an ink annotation
  // that will not be observed for performing undo/redo operations.
  annotation.color = .red
  annotation.lineWidth = 5
}
Copy
1
2
3
4
5
6
PSPDFPerformBlockWithoutUndo(document.undoController, ^{
  // This is an example of how to group actions for an ink annotation
  // that will not be observed for performing undo/redo operations.
  annotation.color = UIColor.redColor;
  annotation.lineWidth = 5.0;
});