不同iOS版本中使用@Published变量Binding的不一致问题排查
问题背景
我实现了一个SwiftUI页面,用已有值预填充自定义文本字段,希望限制某个字段输入不超过10个字符。通过ViewModel的@Published变量,结合其投影值(projectedValue)获取Binding传递给自定义文本字段组件。
这段代码在iOS 16中表现正常,但在iOS 15和iOS 17中,用户可以输入超过10个字符。通过打印日志发现:输入超过限制后,CustomTextField初始化了正确的Binding,但视图的body从未被重新计算。
我已经通过TextField.onChange解决了问题,现在想搞清楚原方法的错误点。
附原代码:
import SwiftUI struct ContentView: View { @ObservedObject private var viewModel: ViewModel init(viewModel: ViewModel) { self.viewModel = viewModel } var body: some View { CustomTextField(binding: $viewModel.firstName) } } struct CustomTextField: View { @Binding var binding: String init(binding: Binding<String>) { _binding = binding print(binding) } var body: some View { getBody() } private func getBody() -> some View { print("getting body") return TextField("", text: $binding) } } class ViewModel: ObservableObject { @Published var firstName: String { didSet { if firstName.count > 10 { firstName = oldValue } } } init(initialFirstNameValue: String) { firstName = initialFirstNameValue } }
原方法的错误核心
1. @Published的触发逻辑限制
@Published的本质是当变量的值发生实际变化时,才会向SwiftUI发送objectWillChange通知,触发依赖该变量的视图刷新。
在代码中,当用户输入超过10字符时,didSet会把firstName回滚到oldValue——此时新值和旧值完全相等,@Published不会发送任何变更通知,视图更新链也就不会被激活。
2. Binding的同步与视图刷新的关联
CustomTextField中的@Binding绑定到viewModel.firstName,但由于@Published没发通知,SwiftUI认为这个状态没有变化,不会触发CustomTextField的body重新计算。而TextField的显示内容依赖于视图刷新时同步的Binding值,因此会停留在用户输入的超长文本状态。
3. iOS版本差异的原因
iOS 16可能在TextField的内部实现中做了特殊处理:即使Binding的wrappedValue被设置为相同的值,也会强制同步显示内容;或者调整了@Published在回滚场景下的触发逻辑(比如即使值未变化也发送通知),因此表现正常。而iOS 15和17遵循了标准的@Published触发规则,导致问题出现。
总结
原方法的核心错误在于:依赖@Published变量的didSet回滚值,但忽略了@Published仅在值实际变更时才会触发视图更新的规则。回滚操作后值未发生变化,导致SwiftUI视图没有刷新,TextField无法同步正确的文本内容。
内容的提问来源于stack exchange,提问作者Spencer DeBuf

