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!
Undo/redo text input w/ SwiftUI TextEditor
1.4k views Asked by Barrrdi At
2
There are 2 answers
0
On
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.