SwiftUI中模型变更后视图未更新问题求助
SwiftUI视图不更新问题
点击重置按钮修改模型后,视图未自动刷新,仅在点击文本框时才会触发刷新,其余功能正常。相关代码如下:
import SwiftUI struct SettingsView: View { @Environment(\.dismiss) private var dismiss @State private var showAlert = false @StateObject private var _model = ViewModel() var body: some View { Form { ForEach($_model.Vars) { v in Section(v.Caption.wrappedValue) { TextField(v.Caption.wrappedValue, text: v.Text) .keyboardType(.decimalPad) } } Button("Save") { for v in _model.Vars { if !v.Read() { showAlert = true return } } let db = Global.Database var settings = db.Settings db.Update { for v in _model.Vars { v.Write(&settings) } } dismiss() } .alert("Invalid numbers", isPresented: $showAlert) { Button("OK") { showAlert = false } } Button("Reset") { Update(RealmSettings()) } }.onAppear() { Update(Global.Database.Settings) } } private func Update(_ settings: RealmSettings) { for v in _model.Vars { v.Update(settings) } } class ViewModel : ObservableObject { @Published var Vars: [VarBase] = [ FloatVar("Rotation Threshold", \.RotationThreshold), FloatVar("Eye Openness Threshold", \.EyeOpennessThreshold), IntVar("Capture Sample Count", \.CaptureSampleCount), IntVar("Lip Distance Threshold (mm)", \.LipDistanceThreshold_mm), FloatVar("Camera Move Threshold (mm)", \.CameraMoveThreshold_mm), FloatVar("Camera Rotation Threshold (deg)", \.CameraRotationThreshold_deg) ] } class VarBase : ObservableObject, Identifiable { let id = UUID() @Published var Text = "" var Caption: String init(_ caption: String) { Caption = caption } func Read() -> Bool { fatalError("Abstract Method") } func Update(_ settings: RealmSettings) { fatalError("Abstract Method") } func Write(_ settings: inout RealmSettings) { fatalError("Abstract Method") } } class Var<T> : VarBase { private var _value: T private let _parser: (String) -> T? private let _toString: (T) -> String private let _keyPath: WritableKeyPath<RealmSettings, T> init( _ value: T, _ caption: String, _ parser: @escaping (String) -> T?, _ toString: @escaping (T) -> String, _ keyPath: WritableKeyPath<RealmSettings, T>) { _value = value _parser = parser _toString = toString _keyPath = keyPath super.init(caption) } override func Read() -> Bool { guard let value = _parser(Text) else { return false } _value = value return true } override func Write(_ settings: inout RealmSettings) { settings[keyPath: _keyPath] = _value } override func Update(_ settings: RealmSettings) { Text = _toString(settings[keyPath: _keyPath]) } } class IntVar : Var<Int> { init(_ caption: String, _ keyPath: WritableKeyPath<RealmSettings, Int>) { super.init(0, caption, { s in Int(s) }, { v in String(v) }, keyPath) } } class FloatVar : Var<Float> { init(_ caption: String, _ keyPath: WritableKeyPath<RealmSettings, Float>) { super.init(0, caption, { s in Float(s) }, { v in String(v) }, keyPath) } } }
问题原因
VarBase是继承自ObservableObject的类,ViewModel的Vars是@Published的数组。当数组内的VarBase实例更新自身的@Published var Text时,SwiftUI不会感知到ViewModel的Vars数组有变化,因此不会触发视图刷新。而点击文本框时,是因为TextField的绑定直接触发了视图更新。
解决方案
方案一:手动触发ViewModel更新通知
在调用Update方法修改模型时,手动发送objectWillChange通知,让SwiftUI感知到ViewModel的变化:
private func Update(_ settings: RealmSettings) { _model.objectWillChange.send() // 手动触发更新通知 for v in _model.Vars { v.Update(settings) } }
方案二:将Var系列改为结构体(值类型)
因为结构体是值类型,当数组内的结构体实例属性变化时,整个数组元素会被视为更新,@Published的数组会自动触发视图刷新。修改步骤:
- 将
VarBase、Var<T>、IntVar、FloatVar从类改为结构体 - 移除
ObservableObject协议,给修改自身属性的方法加上mutating关键字
修改后的核心代码示例:
struct VarBase : Identifiable { let id = UUID() var Text = "" var Caption: String init(_ caption: String) { Caption = caption } func Read() -> Bool { fatalError("Abstract Method") } mutating func Update(_ settings: RealmSettings) { fatalError("Abstract Method") } mutating func Write(_ settings: inout RealmSettings) { fatalError("Abstract Method") } } struct Var<T> : VarBase { private var _value: T private let _parser: (String) -> T? private let _toString: (T) -> String private let _keyPath: WritableKeyPath<RealmSettings, T> init( _ value: T, _ caption: String, _ parser: @escaping (String) -> T?, _ toString: @escaping (T) -> String, _ keyPath: WritableKeyPath<RealmSettings, T>) { _value = value _parser = parser _toString = toString _keyPath = keyPath super.init(caption) } override func Read() -> Bool { guard let value = _parser(Text) else { return false } _value = value return true } override mutating func Write(_ settings: inout RealmSettings) { settings[keyPath: _keyPath] = _value } override mutating func Update(_ settings: RealmSettings) { Text = _toString(settings[keyPath: _keyPath]) } } struct IntVar : Var<Int> { init(_ caption: String, _ keyPath: WritableKeyPath<RealmSettings, Int>) { super.init(0, caption, { s in Int(s) }, { v in String(v) }, keyPath) } } struct FloatVar : Var<Float> { init(_ caption: String, _ keyPath: WritableKeyPath<RealmSettings, Float>) { super.init(0, caption, { s in Float(s) }, { v in String(v) }, keyPath) } }
同时,ViewModel中的Vars数组需要保持@Published,修改Update方法时要确保对数组元素的修改是可变操作:
private func Update(_ settings: RealmSettings) { // 遍历数组并更新每个元素,因为是结构体,需要重新赋值 for index in _model.Vars.indices { var varItem = _model.Vars[index] varItem.Update(settings) _model.Vars[index] = varItem } }
内容的提问来源于stack exchange,提问作者Hadi Fooladi Talari
相关产品推荐
相关产品推荐

