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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 05:45:03