简体   繁体   中英

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!

In respect to using UIViewRepresentable as a TextView or TextField…. this approach works for undo, but not for redo it seems.

The redo button condition undoManager.canRedo seems to change appropriately. However, it doesn't return any undone text into either the textfield or TextView

I'm now wondering is this a bug or something I'm missing in the logic?

 
import SwiftUI
import PlaygroundSupport

class Model: ObservableObject {
    @Published var active = ""
    
    func registerUndo(_ newValue: String, in undoManager: UndoManager?) {
        let oldValue = active
        undoManager?.registerUndo(withTarget: self) { target in
            target.active = oldValue
        }
        active = newValue
    }
}

struct TextView: UIViewRepresentable {
    
    @Binding var text: String
    
    func makeUIView(context: Context) -> UITextView {
        
        
        let textView = UITextView()
        textView.autocapitalizationType = .sentences
        textView.isSelectable = true
        textView.isUserInteractionEnabled = true
        
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
    }
}


struct ContentView: View {
    
    @ObservedObject private var model = Model()
    @Environment(\.undoManager) var undoManager
    @State var text: String = ""
    
    
    var body: some View {
        ZStack (alignment: .bottomTrailing) {
            // Testing TextView for undo & redo functionality
            TextView(text: Binding<String>(
                        get: { self.model.active },
                        set: { self.model.registerUndo($0, in: self.undoManager) }))
            HStack{ 
                // Testing TextField for undo & redo functionality
                TextField("Enter Text...", text: Binding<String>(
                            get: { self.model.active },
                            set: { self.model.registerUndo($0, in: self.undoManager) })).padding()
                Button("Undo") {
                    withAnimation {
                        self.undoManager?.undo()
                    }
                }.disabled(!(undoManager?.canUndo ?? false)).padding()
                Button("Redo") {
                    withAnimation {
                        self.undoManager?.redo()
                    }
                }.disabled(!(undoManager?.canRedo ?? false)).padding()
            }.background(Color(UIColor.init(displayP3Red: 0.1, green: 0.3, blue: 0.3, alpha: 0.3)))
        }.frame(width: 400, height: 400, alignment: .center).border(Color.black)
    }
}

PlaygroundPage.current.setLiveView(ContentView())

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 {
                ToolbarItem {
                    Button {
                        undoManager?.undo()
                    } label: {
                        Image(systemName: "arrow.uturn.left.circle")
                    }
                }
            }
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM