使用SwiftUI TextEditor实现文本输入的撤销/重做功能

3

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


1
目前(SwiftUI 2.0),直接实现这一点是不可能的,因为通过SwiftUI环境可用的UndoManager并非TextEditor的UITextView后端使用的UndoManager。因此,如果您需要撤消/重做操作,请使用自己的UITextView表示形式,然后您将可以访问其undoManager属性。 - Asperi
谢谢Asperi。我就有这种感觉。所以,我通过https://www.appcoda.com/swiftui-textview-uiviewrepresentable/成功实现了这一点——TextView总体上显示/表现良好。但是,由于我的布局在SwiftUI中,我该如何访问它的undoManager呢?例如,将有一个按钮用于撤消和重做,当调用时需要能够引用它。此外,每次按键是否都需要注册以进行撤消,以便有东西可以撤消?还是会自动处理? - Barrrdi
2个回答

3

不可否认,这有点像是一个hack,并且与SwiftUI的风格不太相符,但它确实有效。基本上,在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")
                    }
                }
            }
        }
    }
}

我将此标记为正确答案,因为它完美地解决了撤销操作,这可以说是两个操作中更重要的一个。我假设重做无法正常工作(如https://dev59.com/5r3pa4cB1Zd3GeqPovW5#66201454所示)是我们无法控制的。话虽如此,您似乎表明它也应该起作用,如果确实如此,如果您能回到这个问题,我们将不胜感激。感谢您的时间,并对延迟的确认表示抱歉。 - Barrrdi
我在上面更新了我的代码,添加了一个重做按钮,它运行良好。这是我在应用程序中使用的非常简化版本。它缺少像Coordinator和响应UITextView textViewDidChange这样的东西,但仍然演示了撤消和重做的工作原理。关于另一个答案,它使用自己的撤消管理器并调用registerUndo - 我早期尝试过类似的东西,发现它在各个方面都有问题。这导致我找到了一种解决方案,将UITextView的内部UndoManager暴露给Swift代码。 - Mark Krenek

0
关于使用UIViewRepresentable作为TextView或TextField...这种方法可以用于撤销,但好像不能用于重做。
重做按钮的条件undoManager.canRedo似乎会相应地改变。然而,它不会将任何撤消的文本返回到文本字段或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($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())


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接