在 SwiftUI 中,如何在 TextField 的当前光标位置插入字符串? 在下面的示例中,我想在工具栏按钮事件中将字符串从
1234
更改为 12+34
。
@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.
//
}
}
}
}
}
}
您已经可以访问
inputText
,所以这是确定当前光标位置的问题。正如 这篇 StackOverflow 帖子 中所见,目前这在纯 SwiftUI 中是不可能的。但是,使用自定义实现,您可以通过 String.Index
和 NSTextRange
实现您想要实现的目标。但是,我目前不知道在 SwiftUI 和 AppKit directy 之间传递这个值的方法,所以我下面的实现使用 ObservableObject
单例:
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
}
TextHolder.shared
这里是单例,方便我们通过SwiftUI和AppKit来访问它selectedRange
是用户所选文本的实际NSRange
。我们将使用 location
属性来添加文本,因为这是用户光标所在的位置。justChanged
是反映加号按钮是否被点击的属性,如果是,我们需要将用户的光标向前移动一个点(到加号前面)。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")
}
}
}
}
}
View
用法: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
}
}
这是一个实际的例子: 此代码已通过 Xcode 14.2/macOS 13.1 测试。