Undo and Redo Annotations on iOS
PSPDFKit for iOS supports undo and redo functionality for creating, deleting, and editing annotations, and for form filling. Users can undo and redo changes using the buttons in the annotation toolbar, keyboard shortcuts, or the systemwide UI and gestures.
To achieve this, PSPDFKit uses a standard UndoManager
. We recommend studying its documentation to better understand the principles of how this class works. An undo manager is owned and controlled by an instance of UndoController
.
An undo controller acts as a data source for the undo manager it manages, and it provides several functions for recording undoable commands. Every Document
has its own undo controller, which is exposed via the undoController
property. This allows undo and redo functionality to be disabled on a per-document basis.
Recording Commands
To participate in undo and redo, you need to explicitly record undoable commands using UndoController
. An undoable command may consist of one or more actions described below.
Adding and Removing Annotations
To record an undoable command of adding one or more annotations to a document, wrap an appropriate call in a recording closure, passing an array of annotations expected to be added:
document.undoController.recordCommand(named: "Add Stamp", adding: [stamp]) {
document.add(annotations: [stamp])
}
To record an undoable command of removing one or more annotations from a document, write the following:
document.undoController.recordCommand(named: "Remove Note", removing: [note]) {
document.remove(annotations: [note])
}
In the above examples, add(annotations:)
and remove(annotations:)
can be replaced with equivalent calls to AnnotationManager
or AnnotationProvider
. As long as such a call results in an annotation being added to or removed from a document, an undoable command will be recorded.
Adding annotations using
AnnotationStateManager
will result in undoable commands being automatically recorded. Don’t wrap such calls in a recording closure; otherwise, you’ll end up with duplicates.AnnotationStateManager
is used implicitly when adding annotations using the built-in annotation toolbar.
The title
argument is an optional localized string that can later be displayed in the UI. In the examples above, the recorded undoable commands will be Undo Add Stamp and Undo Remove Note. You can read these titles from the UndoManager
.
You can only record an undoable command for a subset of added or removed annotations. In the following example, an undoable command will only be recorded for one of two annotations:
document.undoController.recordCommand(named: nil, remove: [arrow]) {
document.add(annotations: [arrow, link])
}
Changing Annotations
To record an undoable command of changing multiple properties of one or more annotations, wrap your modifications in a recording closure:
document.undoController.recordCommand(named: "Increase Font Size", changing: [freeText]) { freeText.fontSize += 10 freeText.sizeToFit() }
Changing the stacking order of annotations can’t be recorded as an undoable action at this moment. Please reach out to us if this is a feature you’re interested in adding to your product.
Mixing Actions
Undoable commands don’t necessarily need to consist of just one type of action. You can freely compose them out of multiple actions:
document.undoController.recordCommand(named: "Replace Shape") { recorder in recorder.record(removing: [square]) { document.remove(annotations: [square]) } recorder.record(adding: [circle]) { document.add(annotations: [circle]) } recorder.record(changing: [freeText]) { freeText.contents = "Circle" } }
Recording Actions Continuously
Some changes, like changing opacity using a slider or resizing an annotation, begin at one point in time and end at another. To record an undoable command for continuous actions, use a recorder object instead:
func resizingWillBegin() { // Ask the undo controller for a recorder object and retain it. recorder = document.undoRecorder.beginRecordingCommand(named: "Resize Stamp", changing: [stamp]) } // Let the user resize the annotation in the UI. func resizingDidFinish() { // At the end, commit the recorder from a delegate method or completion closure. recorder.commit() }
Disabling Undo and Redo
Use the UndoManager
directly to call disableUndoRegistration()
. Keep in mind that disabling an undo manager is a balancing operation, meaning enableUndoRegistration()
must be called an equal number of times to reenable it:
document.undoController.undoManager.disableUndoRegistration()
Performing Undo and Redo
Because undoing and redoing while working with documents are crucial and often-used operations, there are many ways in which they can be performed.
Standard Techniques
PSPDFKit is a good citizen and supports the standard techniques of undoing and redoing commands. First of all, users can use Command-Z and Shift-Command-Z to undo and redo commands. These keyboard shortcuts work both on iOS and in Mac Catalyst apps.
On iOS, PSPDFKit additionally supports both Shake to Undo and the three-finger swipe gesture. Shake to Undo can be deactivated by users in the Accessibility settings or programmatically using the applicationSupportsShakeToEdit
property of UIApplication
. The three-finger swipe gesture can be disabled by overriding the editingInteractionConfiguration
property of UIViewController
.
In a Mac Catalyst app, undoing and redoing is also possible using the Edit menu in the app’s menu bar.
From the Annotation Toolbar
Users can use the buttons in the annotation toolbar to perform undo and redo. If you’re using it in your app, you’re all set. If you have a completely custom annotation toolbar, see the Reacting to Changes section to learn how to integrate it with our undo and redo architecture.
Programmatically
Use the UndoManager
directly to check if undoing or redoing is possible, and if so, do it:
if document.undoController.undoManager.canUndo {
document.undoController.undoManager.undo()
}
Reacting to Changes
If you’re implementing your own undo and redo buttons and you’re trying to update the enabled state, use the following delegate method of AnnotationStateManager
, which listens to the appropriate notifications and calls back with the states whenever they change:
func annotationStateManager(_ manager: AnnotationStateManager, didChangeUndoState undoEnabled: Bool, redoState redoEnabled: Bool) { customUndoButton.isEnabled = undoEnabled customRedoButton.isEnabled = redoEnabled }
This class supports multiple delegates. Use
add(_:)
andremove(_:)
to register them.
If you need more detailed control, you can also observe various undo manager notifications. When a new undoable command is recorded, an NSUndoManagerDidCloseUndoGroup
notification will be posted. When a user undoes or redoes a command, NSUndoManagerDidUndoChange
or NSUndoManagerDidRedoChange
will be posted.
See CustomVerticalAnnotationToolbarExample
for a sample implementation of custom undo and redo buttons in the PSPDFKit Catalog.
Automatically Recorded Commands
PSPDFKit integrates the undo and redo functionality into most of the built-in components. The following list outlines the most important actions that result in undoable commands being recorded. If you’re using our stock controls, there’s nothing you need to do to have them in your app.
Adding Annotations
-
Adding annotations using the annotation toolbar or menu
-
Dragging and dropping text, images, or PDF stamps onto a page
-
Pasting previously copied text, images, or annotations
-
Programmatically using
AnnotationStateManager
Removing Annotations
-
Removing annotations using the menu or keyboard shortcuts
-
Clearing annotations from within the annotation list
-
Cutting annotations from pages
Changing Annotations
-
Editing properties using the annotation inspector or the menu
-
Moving, resizing, or rotating annotations
-
Editing contents of free text annotations
-
Adjusting shapes of line, polyline, and polygon annotations
-
Adding, editing, and removing replies and reviews
-
Editing form fields
Further Reading
For more information about the undo and redo architecture, check out the documentation of the UndoController
and UndoRecorder
protocols.