UndoManager On iOS

What is UndoManager?

UndoManager or NSUndoManager (in Objective-C) is one of the data structures that foundations packs. It is a stack that facilitates undo operations.

How does it work?

UndoManager deals with actions and not with states. The developer is responsible for maintaining the state using UndoManager. UndoManager by default groups all operations happening in one cycle. So, when you perform ‘undo’ those actions get reverted together.

Let us detail this with one example.

Can I see an example?

import Foundation

enum Type {
    case book
    case pen
    case pencil
}

class Store: CustomStringConvertible {
    weak var undoManager: UndoManager?
    
    var inventory: [Type: Int] = [
        .book: 24,
        .pen: 8,
        .pencil: 10
    ]
    
    var description: String {
        """
        ----------------------------
        Book  : \(inventory[.book] ?? 0)
        Pen   : \(inventory[.pen] ?? 0)
        Pencil: \(inventory[.pencil] ?? 0)
        ----------------------------
        """
    }
    
    init(undoManager: UndoManager) {
        self.undoManager = undoManager
    }
    
    func remove(_ type: Type, count: Int) {
        store.inventory[type] = max(0, store.inventory[type]! - count)
        undoManager?.beginUndoGrouping()
        undoManager?.registerUndo(withTarget: self) { [weak self] _ in
            guard let self = self else { return }
            self.add(type, count: count)
        }
        undoManager?.endUndoGrouping()
    }
    
    func add(_ type: Type, count: Int) {
        store.inventory[type]! += count
        undoManager?.beginUndoGrouping()
        undoManager?.registerUndo(withTarget: self) { [weak self] _ in
            guard let self = self else { return }
            self.remove(type, count: count)
        }
        undoManager?.endUndoGrouping()
    }
}

let undoManager = UndoManager()
// let us opt manual grouping since everything will run in single cycle
undoManager.groupsByEvent = false   
let store = Store(undoManager: undoManager)

print(store)
store.add(.book, count: 3)
print(store)
store.remove(.pen, count: 2)
print(store)
undoManager.undo()
print(store)
undoManager.undo()
print(store)
undoManager.redo()
print(store)

Notes

While using UndoManager be careful around creating retain cycles.

References

  1. Registering Undo with Target
  2. Introduction to Undo Architecture