You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的数组会自动触发视图刷新。修改步骤:

  1. 将VarBase、Var<T>、IntVar、FloatVar从类改为结构体
  2. 移除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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 12:33:12