SwiftUI,从工具栏按钮更改TextField字符串

huangapple go评论55阅读模式
英文:

SwiftUI, change the TextField string from the toolbar button

问题

在SwiftUI中,您可以如何将字符串插入到TextField的当前光标位置?在下面的示例中,我希望在工具栏按钮事件中将字符串从“1234”更改为“12+34”。

@State private var inputText: String = "1234"

public var body: some View {
    VStack {
        TextField("输入文本", text: $inputText)
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                HStack {
                    Button("+") {
                        //
                        // 在这里,我希望在当前光标位置插入“+”。
                        //
                    }
                }
            }
        }
    }
}

希望这对您有所帮助。

英文:

In SwiftUI, how can I insert a string at the current cursor position of the TextField?
In below example, I want to change the string from 1234 to 12+34 in the toolbar button event.

@State private var inputText: String = "1234"

public var body: some View {
    VStack {
        TextField("Input text", text: $inputText)
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                HStack {
                    Button("+") {
                        //
                        // Here I want to insert "+" at the current cursor position.
                        //
                    }
                }
            }
        }
    }
}

答案1

得分: 1

您已经可以访问inputText,所以这涉及确定当前光标位置。如此StackOverflow帖子所示,目前纯SwiftUI无法实现此功能。然而,使用自定义实现,您可以通过String.IndexNSTextRange来潜在地实现您想要的效果。然而,我目前不知道如何在SwiftUI和AppKit之间直接传递此值,因此下面的实现使用了一个ObservableObject单例

TextHolder

class TextHolder: ObservableObject {
    /// 用于在不同框架之间访问的`TextHolder`的共享实例。
    public static let shared = TextHolder()
    
    /// 当前用户选择的文本范围。
    @Published var selectedRange: NSRange? = nil
    
    // 注意:如果不需要更新光标位置,可以将下一个变量注释掉
    /// SwiftUI是否刚刚更改了文本
    @Published var justChanged = false
}

一些解释:

  • TextHolder.shared 在这里是一个单例,以便我们可以通过SwiftUI和AppKit访问它。
  • selectedRange 是用户选择文本的实际NSRange。我们将使用location属性来添加文本,因为这是用户光标的位置。
  • justChanged 是一个属性,用于反映加号按钮是否刚刚被点击,如果是,我们需要将用户的光标前移一个位置(到加号前面)。

TextFieldRepresentable

struct TextFieldRepresentable: NSViewRepresentable{
    
    
    /// 这是用于SwiftUI的`NSTextField`
    typealias NSViewType = NSTextField
    
    /// 当`text`为空时要显示的占位符
    var placeholder: String = ""
    
    /// 这是`TextFieldRepresentable`将显示和更改的文本。
    @Binding var text: String
    
   
    func makeNSView(context: Context) -> NSTextField {
        let textField = NSTextField()
        // 设置当没有文本时的占位符
        textField.placeholderString = placeholder
        // 设置TextField的代理
        textField.delegate = context.coordinator
        
        return textField
    }
    
    func updateNSView(_ nsView: NSTextField, context: Context) {
        // 更新实际的TextField
        nsView.stringValue = text
        // 注意:如果不需要更新光标位置,可以将此注释掉
        DispatchQueue.main.async {
            // 如果SwiftUI刚刚更改了值,将光标前移一位
            if TextHolder.shared.justChanged{
                nsView.currentEditor()?.selectedRange.location += 1
                TextHolder.shared.justChanged = false
            }
        }
        // 结束可注释区域
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, NSTextFieldDelegate {
        var parent: TextFieldRepresentable
        
        init(_ parent: TextFieldRepresentable) {
            self.parent = parent
        }
        
        func controlTextDidChange(_ obj: Notification) {
            // 为了避免“NSHostingView is being laid out reentrantly while rendering its SwiftUI content.”错误
            DispatchQueue.main.async {
                
                // 确保我们可以获取当前编辑器
                // 如果不能,适当处理错误
                if let textField = obj.object as? NSTextField, let editor = textField.currentEditor(){
                    // 更新父级的文本,以便SwiftUI知道新的值
                    self.parent.text = textField.stringValue
                    // 设置属性
                    TextHolder.shared.selectedRange = editor.selectedRange
                } else {
                    // 处理错误 - 无法获取编辑器
                    print("无法获取当前编辑器")
                }
            }
        }
    }
}

最后,是示例View的用法:

struct ContentView: View {
    @State private var inputText: String = "1234"
    @ObservedObject var holder = TextHolder.shared
    public var body: some View {
        VStack {
            TextFieldRepresentable(placeholder: "输入文本", text: $inputText)
            .toolbar {
                ToolbarItem(id: UUID().uuidString, placement: .automatic) {
                    HStack {
                        Button("+") {
                            insertPlus()
                        }
                    }
                }
            }
        }
    }
    
    /// 在selectedRange位置插入加号字符
    func insertPlus(){
        // 首先,我们将检查我们的范围是否不为空
         guard let selectedRange = holder.selectedRange else {
             // 处理错误,因为无法获取选择的范围
             print("holder中不包含选定范围")
             return
         }
         let endPos = inputText.index(inputText.startIndex, offsetBy: selectedRange.location) // 选择范围的结束位置
         // 插入文本
         inputText.insert(contentsOf: "+", at: endPos)
        // 移动光标到正确位置是必要的
        TextHolder.shared.justChanged = true
    }
}

这是此示例的运行效果。

SwiftUI,从工具栏按钮更改TextField字符串

此代码已在Xcode 14.2/macOS 13.1上进行测试。

来源

英文:

You already have access to the inputText, so this is a matter of determining the current cursor position. As seen in this StackOverflow post, this is currently not possible with pure SwiftUI. However, using a custom implementation, you can potentially achieve what you are trying to achieve, via String.Index and NSTextRange. However, I'm not currently aware of a way to pass this value between SwiftUI and AppKit directly, so my implementation below uses an ObservableObject singleton:

TextHolder

class TextHolder: ObservableObject {
    ///The shared instance of `TextHolder` for access across the frameworks.
    public static let shared = TextHolder()
    
    ///The currently user selected text range.
    @Published var selectedRange: NSRange? = nil
    
    //NOTE: You can comment the next variable out if you do not need to update cursor location
    ///Whether or not SwiftUI just changed the text
    @Published var justChanged = false
}

Some explanations:

  • TextHolder.shared is the singleton here, so that we can access it through SwiftUI and AppKit.
  • selectedRange is the actual NSRange of the user selected text. We will use the location attribute to add text, as this is where the user's cursor is.
  • justChanged is a property that reflects whether or not the plus button was just clicked, as we need to move the user's cursor forward one spot (to in front of the plus) if so.

TextFieldRepresentable

struct TextFieldRepresentable: NSViewRepresentable{
    
    
    ///This is an `NSTextField` for use in SwiftUI
    typealias NSViewType = NSTextField
    
    ///The placeholder to be displayed when `text` is empty
    var placeholder: String = ""
    
    ///This is the text that the `TextFieldRepresentable` will display and change.
    @Binding var text: String
    
   
    func makeNSView(context: Context) -> NSTextField {
        let textField = NSTextField()
        //Set the placeholder for when there is no text
        textField.placeholderString = placeholder
        //Set the TextField delegate
        textField.delegate = context.coordinator
        
        return textField
    }
    
    func updateNSView(_ nsView: NSTextField, context: Context) {
        //Update the actual TextField
        nsView.stringValue = text
        //NOTE: You can comment this out if you do not need to update the cursor location
        DispatchQueue.main.async {
            //Move the cursor forward one if SwiftUI just changed the value
            if TextHolder.shared.justChanged{
                nsView.currentEditor()?.selectedRange.location += 1
                TextHolder.shared.justChanged = false
            }
        }
        //END commentable area
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, NSTextFieldDelegate {
        var parent: TextFieldRepresentable
        
        init(_ parent: TextFieldRepresentable) {
            self.parent = parent
        }
        
        func controlTextDidChange(_ obj: Notification) {
            //To avoid the "NSHostingView is being laid out reentrantly while rendering its SwiftUI content." error
            DispatchQueue.main.async {
                
                //Ensure we can get the current editor
                //If not, handle the error appropriately
                if let textField = obj.object as? NSTextField, let editor = textField.currentEditor(){
                    //Update the parent's text, so SwiftUI knows the new value
                    self.parent.text = textField.stringValue
                    //Set the property
                    TextHolder.shared.selectedRange = editor.selectedRange
                } else {
                    //Handle errors - we could not get the editor
                    print("Could not get the current editor")
                }
            }
        }
    }
}

And finally, the example View usage:

struct ContentView: View {
    @State private var inputText: String = "1234"
    @ObservedObject var holder = TextHolder.shared
    public var body: some View {
        VStack {
            TextFieldRepresentable(placeholder: "Input text", text: $inputText)
            .toolbar {
                ToolbarItem(id: UUID().uuidString, placement: .automatic) {
                    HStack {
                        Button("+") {
                               insertPlus()
                        }
                    }
                }
            }
        }
    }
    
    ///Inserts the plus character at the selectedRange/
    func insertPlus(){
        //First, we will check if our range is not nil
         guard let selectedRange = holder.selectedRange else {
             //Handle errors, as we could not get the selected range
             print("The holder did not contain a selected range")
             return
         }
         let endPos = inputText.index(inputText.startIndex, offsetBy: selectedRange.location) // End of the selected range position
         //Insert the text
         inputText.insert(contentsOf: "+", at: endPos)
        //Necessary to move cursor to correct location
        TextHolder.shared.justChanged = true
    }
}

Here is an example of this in action:

SwiftUI,从工具栏按钮更改TextField字符串

This code has been tested with Xcode 14.2/macOS 13.1.

Source

huangapple
  • 本文由 发表于 2023年2月26日 20:28:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/75571977.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定