SwiftUI MVVM层级无持久化时保留数据及实现问题咨询
问题解答
一、数据丢失的根本原因
你当前遇到的Car丢失问题核心是 值类型的复制特性 导致的:
- 你定义的
Garage和Car都是struct(值类型),当你把Garage实例传给GarageViewModel的构造函数时,实际上是生成了一个独立的副本,你在GarageView里修改的是这个副本,完全没有同步回GarageListViewModel里的garages数组。 - 当你添加新的
Garage时,GarageListViewModel的@Published garages会触发整个列表视图刷新,此时NavigationLink会重新用GarageListViewModel里未更新的原始Garage实例生成新的GarageViewModel,之前你存在副本里的Car自然就消失了。
二、当前MVVM实现的合理性判断
你当前的实现不符合SwiftUI生态下的MVVM最佳实践,核心问题有两个:
- 不必要的ViewModel层级:不需要为每个Model都单独定义一个
ObservableObject类型的ViewModel,尤其是子级Model对应的ViewModel,单独作为ObservableObject会大幅提升状态同步的复杂度。 - 状态没有单一数据源:你现在每个子ViewModel持有独立的Model副本,破坏了单一数据源原则,自然会出现状态不同步的问题。
三、实现View与Model严格分离的改造方案
你可以保留「View不直接接触Model」的要求,改造后依然符合MVVM规范,同时解决数据丢失问题,核心改造点如下:
改造思路
- 只保留顶层的
GarageListViewModel作为唯一的ObservableObject数据源,所有数据修改都走这个ViewModel暴露的方法,子视图不需要持有自己的ObservableObject类型ViewModel。 - 子视图需要的所有数据都由父视图通过ViewModel的方法传递,所有操作都调用父ViewModel暴露的接口,View完全不需要知道Model的内部结构。
改造后的完整代码
import SwiftUI // MARK: - Models (完全不暴露给View) fileprivate struct Car: Identifiable, Codable { var id = UUID() var name: String } fileprivate struct Garage: Identifiable, Codable { var id = UUID() var name: String var cars: [Car] = [] } // MARK: - ViewModel (唯一数据源,暴露给View的只有方法和计算属性) class GarageListViewModel: ObservableObject { @Published private var garages: [Garage] = [] // 把garages私有化,避免View直接接触Model // 暴露给View的车库列表数据,只返回View需要的字段 var garageList: [(id: UUID, name: String)] { garages.map { ($0.id, $0.name) } } // 暴露添加车库的方法 func addGarage() { garages.append(Garage(name: "My Garage")) } // 暴露获取指定车库的车辆列表的方法 func getCarList(for garageId: UUID) -> [(id: UUID, name: String)] { guard let index = garages.firstIndex(where: { $0.id == garageId }) else { return [] } return garages[index].cars.map { ($0.id, $0.name) } } // 暴露给车库视图添加车辆的方法 func addCar(to garageId: UUID) { guard let index = garages.firstIndex(where: { $0.id == garageId }) else { return } garages[index].cars.append(Car(name: "My Car")) } // 暴露获取车辆名称的方法 func getCarName(carId: UUID, in garageId: UUID) -> String { guard let garageIndex = garages.firstIndex(where: { $0.id == garageId }), let car = garages[garageIndex].cars.first(where: { $0.id == carId }) else { return "" } return car.name } } // MARK: - Views (完全不接触Model,只调用ViewModel的方法) struct GarageListView: View { @ObservedObject var viewModel: GarageListViewModel var body: some View { List { ForEach(viewModel.garageList, id: \.id) { garage in NavigationLink { // 只传车库ID和ViewModel引用,不需要传Model GarageView(garageId: garage.id, viewModel: viewModel) } label: { Text(garage.name) } } } .toolbar { Button { viewModel.addGarage() } label: { Label("Add", systemImage: "plus") } } } } struct GarageView: View { let garageId: UUID @ObservedObject var viewModel: GarageListViewModel var body: some View { List { ForEach(viewModel.getCarList(for: garageId), id: \.id) { car in NavigationLink { CarDetailView(garageId: garageId, carId: car.id, viewModel: viewModel) } label: { Text(car.name) } } } .toolbar { Button { viewModel.addCar(to: garageId) } label: { Label("Add", systemImage: "plus") } } } } struct CarDetailView: View { let garageId: UUID let carId: UUID @ObservedObject var viewModel: GarageListViewModel var body: some View { Text(viewModel.getCarName(carId: carId, in: garageId)) } } struct ContentView: View { @StateObject var viewModel = GarageListViewModel() var body: some View { NavigationView { GarageListView(viewModel: viewModel) } } }
改造说明
- 所有Model都用
fileprivate修饰,保证只能在当前文件的ViewModel中访问,View完全接触不到Model的定义和结构。 - 所有对数据的操作都封装在ViewModel的方法里,View只需要调用对应的方法,不需要知道Model的内部属性。
- 全程只有一个ObservableObject数据源,不存在状态不同步的问题,完全解决Car丢失的故障。
内容的提问来源于stack exchange,提问作者Naiki
相关产品推荐
相关产品推荐

