【问题标题】:Undo/redo text input w/ SwiftUI TextEditor使用 SwiftUI TextEditor 撤消/重做文本输入
【发布时间】:2021-02-13 02:55:55
【问题描述】:

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

【问题讨论】:

  • 目前(SwiftUI 2.0)无法直接执行,因为通过 SwiftUI 环境在上下文中可用的 UndoManager 不是 TextEditor 的 UITextView 后端使用的 UndoManager。因此,如果您需要撤消/重做 - 使用自己的 UITextView 可表示,那么您将可以访问其 undoManager 属性。
  • 感谢 Asperi。我有预感会是这样。所以,感谢appcoda.com/swiftui-textview-uiviewrepresentable,我设法做到了这一点——TextView 总体上显示/表现良好。但是考虑到我的布局是在 SwiftUI 中,我如何真正访问它的 undoManager?例如,将有一个用于撤消和重做的按钮,调用时需要能够引用它。此外,是否每个按键都需要注册撤消才能撤消?还是会自动处理?

标签: swift swiftui text-editor undo-redo nsundomanager


【解决方案1】:

关于使用 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())

【讨论】:

    【解决方案2】:

    诚然,这有点 hack 并且不是很 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 {
                    ToolbarItem {
                        Button {
                            undoManager?.undo()
                        } label: {
                            Image(systemName: "arrow.uturn.left.circle")
                        }
                    }
                }
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2017-02-09
      • 2016-10-28
      • 1970-01-01
      • 1970-01-01
      • 2011-11-19
      • 2021-05-23
      • 2017-03-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多