SwiftUI中使用@propertyWrapper封装CurrentValueSubject绑定TextField引发重复更新错误及输入异常的问题咨询
问题原因分析
你的第一个版本之所以能正常工作,核心在于TextField用的自定义Binding实现了「显示与输入分离」:
get取的是vm.outputText(经过过滤后的最终值),所以TextField的显示只会在输入内容长度>5时才更新;set只是把输入内容传递给subject,不会直接修改TextField的显示值,避免了频繁的状态同步。
而第二个版本出问题,主要是两个核心原因:
Binding的双向同步冲突
你直接把$vm.inputText绑给TextField的text参数,但你的@Input包装器的projectedValue是AnyPublisher,这其实不符合TextField需要的Binding<String>类型——编译器实际上自动生成了一个基于wrappedValue的默认Binding:get返回subject.value(用户输入的原始值);set调用wrappedValue的setter,也就是subject.send(newValue),而CurrentValueSubject在send后会立刻更新自身的value。
这就形成了循环:TextField输入时调用setter更新
subject.value,紧接着getter就读到了新值,SwiftUI检测到同一帧内状态多次变化,就抛出了"Bindingaction tried to update multiple times per frame"警告。 日文输入的状态干扰
日文输入法在候选词转换过程中(比如从假名切换到汉字),会多次临时修改TextField的文本值。在第二个版本中,这些临时值会直接同步到subject.value,导致TextField的显示频繁跟随临时值变化,打乱了输入法的正常转换流程,最终出现输入异常。
修复方案
如果想继续用@Input封装CurrentValueSubject,需要保持「TextField显示过滤后的值、输入传递给subject」的逻辑。你可以像第一个版本那样自定义Binding,也可以给@Input加个辅助方法来生成所需的Binding:
@propertyWrapper struct Input<Value> { var wrappedValue: Value { get { subject.value } set { subject.send(newValue) } } var projectedValue: AnyPublisher<Value, Never> { subject.eraseToAnyPublisher() } // 添加生成自定义Binding的方法,分离显示与输入逻辑 func binding<DisplayValue>(get displayValue: @escaping () -> DisplayValue, set inputHandler: @escaping (DisplayValue) -> Void) -> Binding<DisplayValue> { Binding( get: displayValue, set: inputHandler ) } private let subject: CurrentValueSubject<Value, Never> init(wrappedValue: Value) { subject = CurrentValueSubject(wrappedValue) } }
然后在View里这样使用:
struct TextFieldSamples: View { @StateObject var vm = TFViewModel() var body: some View { print(Self._printChanges()) return VStack { TextField("placeholder", text: vm.$inputText.binding( get: { vm.outputText }, set: { vm.inputText = $0 } )) Text(vm.outputText) } } }
这样既保留了@Input对CurrentValueSubject的封装,又维持了第一个版本的核心逻辑,解决了错误和输入异常的问题。
内容的提问来源于stack exchange,提问作者Martin

