英文:
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.Index
和NSTextRange
来潜在地实现您想要的效果。然而,我目前不知道如何在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
}
}
这是此示例的运行效果。
此代码已在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 actualNSRange
of the user selected text. We will use thelocation
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:
This code has been tested with Xcode 14.2/macOS 13.1.
Source
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论