Block Based Undo/Redo Api in SwiftUI

When building applications on macOS, and iOS, it is good practice to include undo/redo support within your application. SwiftUI provides @Environment(.undoManager) this exposes an UndoManager however to register undo actions onto the manager we need to pass a class.

We could use the UndoManager itself however that would result in the undo operations persisting in the undo stack even after the user has moved on to a different view. I have already discussed an UndoProvider based solution that I am using in Cleora however in some situations it is easier to have an explicit undo and redo operation.

To do this we can define a propertyWrapper that captures UndoManager and exposes the method we need.

struct UndoProxy: DynamicProperty {
  var wrappedValue = UndoProxyObject()
  var projectedValue
  mutating func update() {
    self.wrappedValue.undoManager = self.projectedValue

Conforming to DynamicProperty means that SwiftUI will call our update method giving us the chance to assign the UndoManger to the UndoProxyObject.

In the above UndoProxy wrapper we make use of UndoProxyObject.

class UndoProxyObject: ObservableObject {
  weak var undoManager: UndoManager?
  func `do`(
    _ doBlock: @escaping () -> Void,
    undo undoBlock: @escaping () -> Void
  ) {
    undoManager?.registerUndo(withTarget: self) { target in, undo: doBlock)

UndoProxyObject provides a single do method that can be called with do and undo blocks. It executes the do block and then registers an undo operation that flips the do and undo blocks so that the do block becomes the redo action when then undo is being applied.

A good example of the UndoProxy within your views is for adding the ability to undo a delete action on a list of items.

struct ContentView: View {
  var undoProxy
  var body: some View {
    ForEach(users) { user in
    }.onDelete { indices in

      var removedUsers: [User] = []
      for index in indices {
        users.remove(atOffsets: indices)
      } undo: {
        for (index, indexInList) in indices.enumerated() {
          users.insert(removedUsers[index], at: indexInList)