SwiftUI中TextField的onSubmit内State变量不更新问题
问题描述
在SwiftUI视图中维护了两份State变量presets和currentPreset,用于同步EnvironmentObject中的settings数据。但执行重命名操作后,手动赋值更新这两个State变量时,发现它们并未同步最新的name值,NSLog显示State变量与settings中的值不一致,直到重启应用才会同步。
相关代码如下:
视图中的State定义:
@EnvironmentObject var settings: Settings @State var presetname: String = "" @State var presets: [RecordingPreset] = [] @State var currentPreset = RecordingPreset()
提交逻辑:
Section("Rename") { TextField(currentPreset.name!, text: $presetname) .onSubmit { settings.renamePreset(presetname, preset: currentPreset) presets = settings.presets // 未更新 currentPreset = settings.currentPreset // 未更新 NSLog("Updating current preset name to \(currentPreset.name), \(settings.currentPreset.name)") } }
RecordingPreset结构体定义:
public struct RecordingPreset: Equatable, Identifiable { public static func == (lhs: RecordingPreset, rhs: RecordingPreset) -> Bool { return lhs.id == rhs.id } public var id: UUID public var name: String? // 其他属性 }
根本原因
SwiftUI在处理@State变量的赋值时,会通过Equatable协议判断新旧值是否相等。如果判定相等,会跳过State的更新操作(包括不更新视图,也不会更新State内部存储的数值)。
你的RecordingPreset结构体自定义了==方法,仅通过id判断实例是否相等。这导致:
- 当
settings.presets中的元素name被修改后,新数组与原presets数组通过元素的id比较会被判定为相等,SwiftUI跳过presets的更新。 - 同理,
settings.currentPreset的name修改后,与原currentPreset实例因id相同被判定为相等,SwiftUI也跳过currentPreset的更新。
优化解决方案
方案1:修正Equatable实现(推荐)
让RecordingPreset的==方法比较所有影响视图展示的字段,而不只是id。这样当name或其他属性变化时,实例会被判定为不相等,SwiftUI能正确检测到State的变化并更新。
修改后的结构体:
public struct RecordingPreset: Equatable, Identifiable { public static func == (lhs: RecordingPreset, rhs: RecordingPreset) -> Bool { return lhs.id == rhs.id && lhs.name == rhs.name // 若有其他需要比较的属性,一并加入判断 } public var id: UUID public var name: String? // 其他属性 }
方案2:移除视图中的State变量,直接依赖EnvironmentObject(更优)
视图中无需维护presets和currentPreset的State副本,直接使用settings环境对象中的@Published属性。这样当settings中的数据变化时,视图会自动更新,从根源上避免同步问题。
步骤1:确保Settings类的属性是@Published
class Settings: ObservableObject { @Published var presets: [RecordingPreset] = [] @Published var currentPreset: RecordingPreset = RecordingPreset() func renamePreset(_ newName: String, preset: RecordingPreset) { // 找到目标预设并更新name if let index = presets.firstIndex(where: { $0.id == preset.id }) { var updatedPreset = presets[index] updatedPreset.name = newName presets[index] = updatedPreset // 如果是当前预设,同步更新currentPreset if currentPreset.id == preset.id { currentPreset = updatedPreset } } } }
步骤2:修改视图代码,直接使用settings中的值
@EnvironmentObject var settings: Settings @State var presetname: String = "" var body: some View { Section("Rename") { TextField(settings.currentPreset.name!, text: $presetname) .onSubmit { settings.renamePreset(presetname, preset: settings.currentPreset) presetname = "" // 可选:清空输入框 } } }
方案3:强制触发State更新(临时方案,不推荐)
如果无法修改Equatable实现或Settings类,可以通过创建新的实例来打破Equatable的判断,强制SwiftUI更新State:
// 更新presets时,创建新数组实例 presets = settings.presets.map { $0 } // 或 presets = Array(settings.presets) // 更新currentPreset时,创建新实例 var updatedPreset = settings.currentPreset currentPreset = updatedPreset
这种方法通过创建新的值类型实例,让SwiftUI判定新旧值不相等,从而触发更新,但属于hack手段,不如前两种方案优雅。
内容的提问来源于stack exchange,提问作者Deepak Sharma

