SwiftUI Undo/Redo Buttons for TextEditor/Keyboard

I'm using SwiftUI's simple TextEditor setup:

struct TextEditorView: View {

  @State var fullText: String = ""

  var body: some View {
    TextEditor(text: $fullText)
    .overlay(
    HStack{
        Button(action:{undoType()})
        {Image(systemName:"arrow.uturn.backward")}
        Button(action:{redoType()})
        {Image(systemName:"arrow.uturn.forward")}   
     ,alignment:.bottomTrailing)

  }

 func undoType(){
    //TODO: Undo the last typed edit
    }

func redoType(){
    //TODO: Redo the last typed edit
    }

}

Question:

I'm asking how to implement simple Undo and Redo buttons in this TextEditorView for the iPhone specifically.

Note:

  • Swipe gestures for undo/redo function work perfectly. Three-finger swipe will undo/redo in this View.
  • Undo/redo buttons appear on the iPad keyboard and work perfectly.
  • (Command + Z) Keyboard command works when using an external keyboard.

Any thoughts?

I would try something like this:

import SwiftUI

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
struct ContentView: View {
    var body: some View {
        TextEditorView()
    }
}
struct TextEditorView: View {
    @State private var fullText: String = ""
    @State private var undoText: [Character] = []

    var body: some View {
        VStack {
            HStack {
                Button(action: undoType) {Image(systemName:"arrow.uturn.backward")}
                Button(action: redoType) {Image(systemName:"arrow.uturn.forward")}
            }
            TextEditor(text: $fullText)
        }
    }

    func undoType(){
        if let lastChar = fullText.last {
            undoText.append(lastChar)
            fullText = String(fullText.dropLast())
        }
    }
    func redoType(){
        if let lastChar = undoText.last {
            undoText.removeLast()
            fullText.append(lastChar)
        }
    }
}

You need access to the internal UndoManager that TextEditor uses. Then you can use it to configure your undo button and to call undo()/redo() on, BUT I don't know of a way to get to that UndoManger.

You can however, create your own UIViewRepresentable that wraps a UITextView and then get access to the internal undo manager that UITextView uses.

Here’s how I did that:

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.

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 {
                ToolbarItem {
                    Button {
                        undoManager?.undo()
                    } label: {
                        Image(systemName: "arrow.uturn.left.circle")
                    }
                }
            }
        }
    }
}
SwiftUI Undo/Redo Buttons for TextEditor/Keyboard
 
 
Q