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(_:) and remove(_:) 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

Removing Annotations

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.