PSPDFKit 10.4 Migration Guide

This guide covers updating an iOS or Mac Catalyst project from PSPDFKit 10.3 for iOS to PSPDFKit 10.4 for iOS. We encourage you to update as soon as possible, in order to take advantage of future new features and fixes.

Cache Initializers

In PSPDFKit 10.4 for iOS, we removed public APIs for initializing PDFCache, DiskCache, and MemoryCache because PSPDFKit handles creating these objects internally, and there shouldn’t be a need to initialize them.

To access the globally used cache, you can use SDK.shared.cache.

New Undo and Redo Architecture

With PSPDFKit 10.4 for iOS, registering and performing undo and redo operations in Document has been completely revamped to be more powerful, performant, and customizable.

Recording Commands Is Now Opt-In

The most important feature of the new undo and redo mechanism is that it’s now opt-in. Undoable commands will no longer be picked up automatically based on key-value observing. Instead, they need to be explicitly recorded, most often as a result of a user action.

PSPDFKit 10.4 for iOS adds support for recording undoable commands for adding, removing, and changing annotations. Unless registering actions is disabled in the undo manager, PSPDFKit will record these as a result of most user interactions through the built-in UI.

Adding and Removing Annotations

To record an undoable command of adding one or more annotations to a document, you can wrap your code in the following way:

document.undoController.recordCommand(named: "Insert Note", adding: [note]) {
    document.add(annotations: [note])
}

Similarly, to record an undoable command of removing one or more annotations from a document, you can write the following:

document.undoController.recordCommand(named: "Remove Shapes", removing: [square, circle]) {
    document.remove(annotations: [square, circle])
}

The add(annotations:options:) and remove(annotations:options:) calls 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.

Note that adding and removing annotations using AnnotationStateManager will result in undoable commands being automatically recorded.

Changing Annotations

To record an undoable command of changing properties of one or more annotations, you can write:

document.undoController.recordCommand(named: "Increase Font Size", changing: [freeText]) {
    freeText.fontSize += 10
    freeText.sizeToFit()
}

On a technical level, the new undo and redo architecture will perform a diff of properties affected by the execution of the given scope closure and append an appropriate undoable command to the UndoManager’s stack.

Migration Cases

This section explores various use cases of the old undo and redo architecture, and it describes how to achieve the same results using the new API.

Disabling Undo and Redo

Use the UndoManager directly to call disableUndoRegistration(). Note that disabling an undo manager is a balancing operation, meaning enableUndoRegistration() must be called an equal number of times to reenable it:

// Previously you would have...
document.isUndoEnabled = false
// Now you can write...
document.undoController.undoManager.disableUndoRegistration()

Performing Non-Undoable Operations

Since undoable commands are no longer automatically recorded, you can perform non-undoable operations by not asking UndoController to record them:

// Previously you would have...
UndoController.performWithoutUndo(undoController: document.undoController) {
    line.fillColor = .red
    line.alpha = 0.5
)
// Now you can write...
line.fillColor = .red
line.alpha = 0.5

Grouping Multiple Actions Together

Use UndoController to record undoable commands with multiple actions:

// Previously you would have...
UndoController.performAsGroup(
    undoController: document.undoController,
    closure: {
        firstFreeText.contents += secondFreeText.contents
        document.remove(annotations: [firstFreeText])
    },
    name: "Merge Contents"
)
// Now you can write...
document.undoController.recordCommand(named: "Merge Contents") { recorder in
    recorder.record(changing: [firstFreeText]) {
        firstFreeText.contents += secondFreeText.contents
    }
    recorder.record(removing: [secondFreeText]) {
        document.remove(annotations: [secondFreeText])
    }
}

Performing Undo and Redo

Use the UndoManager directly to check if undoing or redoing is possible, and if so, do it:

// Previously you would have...
if document.undoController.canUndo {
    document.undoController.undo()
}
if document.undoController.canRedo {
    document.undoController.redo()
}
// Now you can write...
if document.undoController.undoManager.canUndo {
    document.undoController.undoManager.undo()
}
if document.undoController.undoManager.canRedo {
    document.undoController.undoManager.redo()
}

Reacting to State Changes

Previously, you’d use UndoController.didAddUndoAction and UndoController.didRemoveUndoAction notifications to react to the state changes inside an undo controller. These notifications have been removed.

Implementing the annotationStateManager(_:didChangeUndoState:redoState:) delegate method of AnnotationStateManager is now the recommended way to ensure your custom UI stays up to date with the state of the undo manager.

You can also observe various undo manager notifications if you need more detailed control. In particular, an NSUndoManagerDidCloseUndoGroup notification will be posted whenever a new undoable command is recorded.

Custom Annotation Subclasses

If you define custom annotation subclasses, previously you had to override UndoSupport protocol requirements — most importantly, the keysForValuesToObserveForUndo() function — to inform the undo and redo architecture about any new properties that should be taken into consideration when observing changes made to an annotation. This protocol has been removed, and the same can now be achieved by overriding the propertyKeysForUndo property:

// Previously you would have...
override static func keysForValuesToObserveForUndo() -> Set<String> {
    super.keysForValuesToObserveForUndo().union([
        "addedProperty",
    ])
}
// Now you can write...
override static var propertyKeysForUndo: Set<String> {
    super.propertyKeysForUndo.union([
        "addedProperty",
    ])
}

Further Reading

For more information about the new undo and redo architecture, check out our Undo and Redo guide and the documentation of UndoController.

Breaking API Changes