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

SwiftUI中@StateObject不更新视图、@ObservedObject重复创建ViewModel的解决方法

SwiftUI MVVM:按需更新ViewModel并复用状态

问题描述

我希望采用MVVM架构分离UI展示逻辑与视图本身,当前实现如下:

ViewA代码:

struct ViewA: View {
    @StateObject var viewModel: ViewModel
    
    init(titleList: [String]) {
        let viewModel = ViewModel(titleList: titleList)
        self._viewModel = .init(wrappedValue: viewModel)
    }
    
    var body: some View {
        HStack {
            ForEach(viewModel.titleList, id: \.self) { title in
                Text(title)
            }
        }
    }
}

对应的ViewModel:

extension ViewA {
    class ViewModel: ObservableObject {
        let titleList: [String]
        
        init(titleList: [String]) {
            self.titleList = titleList
            // 假设这里有针对titleList的耗时操作
        }
    }
}

当前存在的问题:

  • 使用@StateObject持有ViewModel时,父视图传递的titleList变化后,ViewA不会创建新的ViewModel,导致视图无法更新;
  • 改为@ObservedObject后,视图能正常更新,但每次渲染都会新建ViewModel,即使titleList未变化,导致VM内的耗时操作重复执行。

需求:仅在titleList变化时触发VM内的耗时操作并更新视图,titleList不变时复用ViewModel的状态。

补充说明:titleList来自父视图,ViewModel不会修改该列表;采用MVVM的目的是将GeometryReader数据计算、视图显示判断、交互事件处理、动画逻辑等从视图中剥离。

视图使用场景示例:

struct ContentView: View {
    @State var titleList = ["First", "Second", "Third"]
    
    var body: some View {
        VStack {
            ViewA(titleList: titleList)
            Text(titleList.joined(separator: ", "))
            
            Button("Update") {
                titleList = ["One", "Two", "Three"]
            }
        }
    }
}

点击按钮后,拼接文本更新,但ViewA不会更新。

解决方案

方案一:使用.id()修饰符强制视图重建(推荐,适合必须重建VM的场景)

SwiftUI中,当视图的id发生变化时,会销毁旧的视图实例并创建新的实例,此时@StateObject会重新初始化ViewModel。我们可以将titleList作为ViewA的id,这样只有当titleList变化时,才会重建ViewA和对应的ViewModel,触发耗时操作;titleList不变时,视图和VM都会被复用。

修改ContentView中ViewA的调用:

ViewA(titleList: titleList)
    .id(titleList) // 用titleList作为视图唯一标识

这种方法逻辑简单,完全匹配需求:titleList变化时重建VM并执行耗时操作,不变时复用现有状态。

方案二:在ViewModel内部更新属性(适合无需重建VM的场景)

如果不需要重建ViewModel,而是希望在现有VM中更新titleList并触发耗时操作,可以将titleList改为@Published属性,通过didSet触发耗时逻辑,同时在View中监听titleList的变化来更新VM的属性。

修改ViewModel:

extension ViewA {
    class ViewModel: ObservableObject {
        @Published var titleList: [String] {
            didSet {
                // titleList变化时触发耗时操作
                performExpensiveOperation()
            }
        }
        
        init(titleList: [String]) {
            self.titleList = titleList
            performExpensiveOperation() // 初始化时执行一次
        }
        
        private func performExpensiveOperation() {
            // 模拟耗时操作
            print("执行耗时操作,处理标题列表:\(titleList)")
        }
    }
}

修改ViewA,添加onChange监听父视图传递的titleList,更新VM的属性:

struct ViewA: View {
    @StateObject private var viewModel: ViewModel
    private let externalTitleList: [String]
    
    init(titleList: [String]) {
        self.externalTitleList = titleList
        self._viewModel = .init(wrappedValue: ViewModel(titleList: titleList))
    }
    
    var body: some View {
        HStack {
            ForEach(viewModel.titleList, id: \.self) { title in
                Text(title)
            }
        }
        .onChange(of: externalTitleList) { newList in
            viewModel.titleList = newList
        }
    }
}

这种方法不需要重建视图和VM,仅更新VM的属性,适合VM内部有需要保留的状态(比如交互状态、动画进度等)的场景。

验证效果

两种方案都能实现:

  • 点击Update按钮后,titleList变化,ViewA会更新显示新的标题;
  • 父视图因其他原因重绘时(比如其他状态变化),只要titleList不变,ViewModel不会重建,耗时操作也不会重复执行。

内容的提问来源于stack exchange,提问作者Diego A. Rincon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 01:50:21