Reading time: 1 min

This recipe shows how to implement undo and redo with SwiftUI TextField.

The end result looks like this:

preview

This solution works for SwiftUI 2+ (iOS 14+, macOS 11+).

Here's the full recipe:

  1. You can access the view's UndoManager from the environment, using the undoManager key.
  2. The UndoManager's registerUndo method requires a target that needs to be a class, so we'll use a separate @StateObject to hold this info. If you're following MVVM, the View's ViewModel is a perfect place for that.
  3. In the TextField itself, pass a custom Binding that interacts with the UndoManager when the text is changed.

Check out the code for more details:

class ViewModel: ObservableObject {
  @Published var text = ""

  func registerUndo(_ newValue: String, in undoManager: UndoManager?) {
    let oldValue = text
    undoManager?.registerUndo(withTarget: self) { [weak undoManager] target in
      target.text = oldValue // registers an undo operation to revert to old text
      target.registerUndo(oldValue, in: undoManager) // this makes redo possible
    }
    text = newValue // update the actual value
  }
}

struct UndoTest: View {
  @Environment(\.undoManager) var undoManager
  @StateObject private var model = ViewModel()

  var body: some View {
    VStack(spacing: 20) {
      TextField("Undo/redo test", text: Binding<String>(
        get: {
          model.text // retrieve the value
        }, set: {
          model.registerUndo($0, in: undoManager) // set the value
        }))
      Button("Undo") {
        undoManager?.undo()
      }
      .disabled(undoManager?.canUndo == false)
      Button("Redo") {
        undoManager?.redo()
      }
      .disabled(undoManager?.canRedo == false)
    }
    .padding()
  }
}

Next Post Previous Post