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

SwiftUI中更新@Observable ViewModel属性为何触发所有关联视图重绘?

问题原因分析

你遇到的问题本质是父视图TestAlarms通过@Bindable观察整个AlarmLevelSetupViewModel对象,导致ViewModel的任意属性变更都会触发父视图body重计算,进而重新初始化两个AlarmLevelRowView实例。尽管SwiftUI自带视图diffing优化,但如果子视图未做依赖隔离,就会出现无关视图也重绘的情况。

解决方案

推荐两种实用的解决思路:

1. 让子视图遵循Equatable并使用EquatableView包裹

通过让AlarmLevelRowView实现Equatable协议,明确视图参数的相等判定规则,再用EquatableView包裹子视图,SwiftUI会自动跳过参数未变化的视图重绘。

步骤1:修改AlarmLevelRowView实现Equatable

struct AlarmLevelRowView: View, Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.title == rhs.title &&
        lhs.color == rhs.color &&
        lhs.radiationType == rhs.radiationType &&
        lhs.values == rhs.values &&
        lhs.selectedValue.wrappedValue == rhs.selectedValue.wrappedValue
    }

    let title: String
    let color: Color
    let radiationType: RadiationType // 假设这是你的自定义枚举类型
    let values: [Decimal]
    @Binding var selectedValue: Decimal

    var body: some View {
        // 原视图渲染逻辑
    }
}

步骤2:在父视图中用EquatableView包裹子视图

struct TestAlarms: View {
    @Bindable var viewModel: AlarmLevelSetupViewModel
    let values: [Decimal] = [0, 1, 2, 3, 4, 5]
    
    var body: some View {
        EquatableView(content: AlarmLevelRowView(
            title: "level 2",
            color: .accentRed,
            radiationType: .radiationLevel(.countRate),
            values: values,
            selectedValue: $viewModel.displayedValueLevel2
        ))

        EquatableView(content: AlarmLevelRowView(
            title: "level 1",
            color: .accentYellow,
            radiationType: .radiationLevel(.countRate),
            values: values,
            selectedValue: $viewModel.displayedValueLevel1
        ))
    }
}

当displayedValueLevel1变更时,第一个AlarmLevelRowView的所有参数均未变化,EquatableView会直接跳过它的重绘流程。

2. 拆分ViewModel为独立的可观察对象

将两个属性拆分到独立的@Observable类中,让父视图分别持有两个@Bindable对象,从根源上隔离属性的观察范围,单个属性的变更只会触发对应子视图重绘。

步骤1:定义单个等级的ViewModel

@Observable
final class SingleAlarmLevelViewModel {
    var displayedValue: Decimal
    init(initialValue: Decimal) {
        self.displayedValue = initialValue
    }
}

步骤2:修改父视图使用拆分后的ViewModel

struct TestAlarms: View {
    @Bindable var level1VM: SingleAlarmLevelViewModel
    @Bindable var level2VM: SingleAlarmLevelViewModel
    let values: [Decimal] = [0, 1, 2, 3, 4, 5]
    
    init() {
        self.level1VM = SingleAlarmLevelViewModel(initialValue: 1)
        self.level2VM = SingleAlarmLevelViewModel(initialValue: 2)
    }
    
    var body: some View {
        AlarmLevelRowView(
            title: "level 2",
            color: .accentRed,
            radiationType: .radiationLevel(.countRate),
            values: values,
            selectedValue: $level2VM.displayedValue
        )

        AlarmLevelRowView(
            title: "level 1",
            color: .accentYellow,
            radiationType: .radiationLevel(.countRate),
            values: values,
            selectedValue: $level1VM.displayedValue
        )
    }
}

内容的提问来源于stack exchange,提问作者Matvei Kozin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 00:45:04