Undo/redo 使用 SwiftUI TextEditor 的文本输入

Undo/redo text input w/ SwiftUI TextEditor

诚然,这是一个广泛的问题,但在使用 SwiftUI TextEditor 控件时是否可以撤消或重做文本输入(通过 iOS 的 UndoManager?)?我到处都看过,但找不到 任何 专注于此工作流组合(SwiftUI + TextEditor + UndoManager)的资源。我想知道鉴于 TextEditor 的相对不成熟,这根本不可能,或者需要一些管道工作来促进。任何指导将不胜感激!

关于将 UIViewRepresentable 用作 TextView 或 TextField…。这种方法适用于撤消,但似乎不适用于重做。

重做按钮条件 undoManager.canRedo 似乎已适当更改。但是,它不会 return 任何未完成的文本到文本字段或 TextView

我现在想知道这是一个错误还是我在逻辑上遗漏了什么?

 
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([=10=], in: self.undoManager) }))
            HStack{ 
                // Testing TextField for undo & redo functionality
                TextField("Enter Text...", text: Binding<String>(
                            get: { self.model.active },
                            set: { self.model.registerUndo([=10=], 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())

诚然,这有点乱七八糟,不是很 SwiftUI-y,但它确实有效。基本上在 UITextView:UIViewRepresentable 中声明绑定到 UndoManager。您的 UIViewRepresentable 会将绑定设置为 UITextView 提供的 UndoManager。然后你的父视图可以访问内部的 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 {
                ToolbarItemGroup(placement: .navigationBarTrailing) {
                    Button {
                        undoManager?.undo()
                    } label: {
                        Image(systemName: "arrow.uturn.left.circle")
                    }
                    Button {
                        undoManager?.redo()
                    } label: {
                        Image(systemName: "arrow.uturn.right.circle")
                    }
                }
            }
        }
    }
}