SwiftUI中@StateObject不更新视图、@ObservedObject重复创建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

