SwiftUI TextField去除首尾空格异常及界面更新问题咨询
问题代码
struct ContentView: View { @State private var name = "" { didSet { print("didSet, name = '\(name)'") } } var body: some View { VStack() { TextField("Name", text: $name, onEditingChanged: { (didBegin) in if !didBegin { print("editing did end. name before trim = '\(name)'") name = name.trimWhiteSpace() print("editing did end. name after trim = '\(name)'") } }, onCommit: { print("committed. name before trim = '\(name)'") name = name.trimWhiteSpace() print("committed. name after trim = '\(name)'") }) .textFieldStyle(.roundedBorder) Text("name = '\(name)'") } .padding([.leading, .trailing]) } } extension String { func trimWhiteSpace() -> String { return self.trimmingCharacters(in: .whitespacesAndNewlines) } }
控制台输出
committed. name before trim = ' Bob ' <- leading and trailing spaces
didSet, name = 'Bob' <- trim was called, looks good
committed. name after trim = 'Bob' <- name appears to be trimmed
editing did end. name before trim = ' Bob ' <- but wait, it's not trimmed
didSet, name = 'Bob' <- trim was called (again), looks good (again)
editing did end. name after trim = 'Bob' <- name appears to be trimmed
问题解答
1. 为何onCommit中已将name修剪,但随后触发的onEditingChanged中name又变回未修剪状态?
这是SwiftUI TextField的回调执行顺序和内部状态同步机制导致的:
- 按下回车键时,onCommit会先触发,你在这里修改
name为修剪后的值,didSet正常执行。 - 但紧接着,TextField在结束编辑状态时,会将它内部编辑缓冲区里的原始未修剪文本重新同步到绑定的
name变量上,所以当onEditingChanged(此时didBegin为false)触发时,拿到的是这个未修剪的原始值。 - 简单来说:onCommit修改了状态,但TextField后续的结束编辑流程又把原始值塞回了绑定变量,导致你在onEditingChanged里看到的是未修剪的内容。
2. 按下回车键后界面仍显示首尾空格,是name实际未修剪还是界面未更新?该如何解决此问题?
界面显示空格是因为TextField的显示内容和内部编辑缓冲区的同步时机问题——你在回调里修改name的时机晚于TextField更新自身显示的时机,导致界面停留在原始未修剪状态。
推荐两种更可靠的解决方式:
方式一:自定义Binding,在setter中自动修剪
直接在绑定的setter里处理修剪,从根源上保证存入name的都是修剪后的值,无需依赖回调:
struct ContentView: View { @State private var name = "" { didSet { print("didSet, name = '\(name)'") } } private var trimmedName: Binding<String> { Binding( get: { name }, set: { newValue in name = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } ) } var body: some View { VStack() { TextField("Name", text: trimmedName) .textFieldStyle(.roundedBorder) Text("name = '\(name)'") } .padding([.leading, .trailing]) } } extension String { func trimWhiteSpace() -> String { return self.trimmingCharacters(in: .whitespacesAndNewlines) } }
方式二:使用FocusState监听编辑结束,统一处理修剪
通过FocusState跟踪TextField的焦点状态,当焦点失去时(编辑结束)执行修剪,避免回调顺序问题:
struct ContentView: View { @State private var name = "" { didSet { print("didSet, name = '\(name)'") } } @FocusState private var isTextFieldFocused: Bool var body: some View { VStack() { TextField("Name", text: $name) .textFieldStyle(.roundedBorder) .focused($isTextFieldFocused) .onChange(of: isTextFieldFocused) { oldValue, newValue in // 当焦点从TextField移开时(编辑结束) if oldValue && !newValue { name = name.trimmingCharacters(in: .whitespacesAndNewlines) } } Text("name = '\(name)'") } .padding([.leading, .trailing]) } } extension String { func trimWhiteSpace() -> String { return self.trimmingCharacters(in: .whitespacesAndNewlines) } }
这两种方式都能避免原代码中回调顺序导致的状态回滚问题,确保界面和name变量始终保持一致的修剪后状态。
内容的提问来源于stack exchange,提问作者Murray Sagal

