swiftswiftuitext-editorundo-redonsundomanager

Undo/redo text input w/ SwiftUI TextEditor


Admittedly this is a broad question, but is it possible to undo or redo text input (via iOS's UndoManager?) when using a SwiftUI TextEditor control? I've looked everywhere and was unable to find any resource focusing on this workflow combination (SwiftUI + TextEditor + UndoManager). I'm wondering given the relative immaturity of TextEditor that either this isn't possible at all, or requires some plumbing work to facilitate. Any guidance will be greatly appreciated!


Solution

  • Admittedly, this is a bit of a hack and non very SwiftUI-y, but it does work. Basically declare a binding in your UITextView:UIViewRepresentable to an UndoManager. Your UIViewRepresentable will set that binding to the UndoManager provided by the UITextView. Then your parent View has access to the internal UndoManager. Here's some sample code. Redo works as well although not shown here.

    struct MyTextView: UIViewRepresentable {
    
        /// The underlying UITextView. This is a binding so that a parent view can access it. You do not assign this value. It is created automatically.
        @Binding var undoManager: UndoManager?
    
        func makeUIView(context: Context) -> UITextView {
            let uiTextView = UITextView()
    
            // Expose the UndoManager to the caller. This is performed asynchronously to avoid modifying the view at an inappropriate time.
            DispatchQueue.main.async {
                undoManager = uiTextView.undoManager
            }
    
            return uiTextView
        }
    
        func updateUIView(_ uiView: UITextView, context: Context) {
        }
    
    }
    
    struct ContentView: View {
    
        /// The underlying UndoManager. Even though it looks like we are creating one here, ultimately, MyTextView will set it to its internal UndoManager.
        @State private var undoManager: UndoManager? = UndoManager()
    
        var body: some View {
            NavigationView {
                MyTextView(undoManager: $undoManager)
                .toolbar {
                    ToolbarItemGroup(placement: .navigationBarTrailing) {
                        Button {
                            undoManager?.undo()
                        } label: {
                            Image(systemName: "arrow.uturn.left.circle")
                        }
                        Button {
                            undoManager?.redo()
                        } label: {
                            Image(systemName: "arrow.uturn.right.circle")
                        }
                    }
                }
            }
        }
    }