SwiftUI视图模型中应存放何种数据引用?
这不仅是理论问题,更是实际设计中的棘手难题:我正纠结于如何将应用的核心数据模型安全地暴露给UI。不管这类中介对象叫什么(比如视图模型),它们需要承担两个核心职责:一是作为UI和核心模型的中间层,处理用户操作引发的数据模型变更;二是确保视图始终展示实时数据——哪怕数据是在后台更新的(比如后台下载消息更新模型,或是用户查看的User对象发生变更)。
更麻烦的是,SwiftUI只能监测值类型的变更,类类型的子对象变更无法自动传播到父对象层面,导致UI无法刷新。苹果在SwiftUI教程里倡导“单一数据源”理念,但考虑到UI和核心模型之间必须有中介层,我实在搞不懂该怎么落地。直接把UI绑定到作为单一数据源的核心模型显然不可取,我到底忽略了什么?
补充说明:我没有使用CoreData这类框架,因为需要跨平台移植,想避免专有绑定,目标平台是iOS 15+。我目前想到的是用通知或代理模式提醒所有视图模型核心模型已变更,但之后视图模型该怎么做?是切换内部标志触发UI刷新?刷新的数据从哪来?是在视图模型里写包装函数从核心模型拉取数据吗?
可行解决方案:基于ObservableObject的中介层+数据订阅模式
1. 核心模型适配:给类添加变更通知机制
既然核心模型是类,我们可以通过Combine框架的PassthroughSubject来广播变更事件(iOS13+支持,满足iOS15+要求),所有修改核心模型的操作都要触发通知:
import Combine // 核心数据模型(跨平台可复用的纯Swift类) class CoreUserModel { var id: String var name: String var avatarURL: String // 用于广播变更的Subject private let changeSubject = PassthroughSubject<Void, Never>() var changePublisher: AnyPublisher<Void, Never> { changeSubject.eraseToAnyPublisher() } init(id: String, name: String, avatarURL: String) { self.id = id self.name = name self.avatarURL = avatarURL } // 业务方法:修改名字并触发变更 func updateName(_ newName: String) { name = newName changeSubject.send() } // 后台更新时调用该方法,触发变更通知 func syncWithBackgroundData(_ data: [String: Any]) { if let newName = data["name"] as? String { name = newName } if let newAvatar = data["avatarURL"] as? String { avatarURL = newAvatar } changeSubject.send() } }
2. 视图模型(中介层):订阅核心模型,维护UI数据
视图模型作为ObservableObject,订阅核心模型的变更事件,实时同步数据到自身的@Published属性,同时封装用户操作转发给核心模型,隔离UI与核心模型的直接耦合:
class UserViewModel: ObservableObject { @Published var displayName: String @Published var avatarURL: String private let coreModel: CoreUserModel private var cancellables = Set<AnyCancellable>() init(coreModel: CoreUserModel) { self.coreModel = coreModel // 初始化时同步核心模型数据 self.displayName = coreModel.name self.avatarURL = coreModel.avatarURL // 订阅核心模型变更,主线程更新UI属性 coreModel.changePublisher .receive(on: DispatchQueue.main) .sink { [weak self] in guard let self = self else { return } self.displayName = self.coreModel.name self.avatarURL = self.coreModel.avatarURL } .store(in: &cancellables) } // 封装用户操作,转发给核心模型 func changeUserName(_ newName: String) { coreModel.updateName(newName) } }
3. SwiftUI视图:绑定视图模型实现自动刷新
视图只需绑定到视图模型的@Published属性,就能自动响应数据变更:
struct UserView: View { @StateObject var viewModel: UserViewModel var body: some View { VStack(spacing: 16) { AsyncImage(url: URL(string: viewModel.avatarURL)) { image in image.resizable().scaledToFill().frame(width: 80, height: 80).clipShape(Circle()) } placeholder: { Circle().fill(.gray).frame(width: 80, height: 80) } Text(viewModel.displayName) .font(.title) Button("修改名字") { viewModel.changeUserName("新昵称") } } .padding() } } // 使用示例 let coreUser = CoreUserModel(id: "1", name: "张三", avatarURL: "https://example.com/avatar.jpg") let userVM = UserViewModel(coreModel: coreUser) UserView(viewModel: userVM)
4. 无Combine替代方案:用通知中心实现
如果不想依赖Combine,可通过通知中心完成核心模型的变更通知:
- 核心模型更新时发送通知:
// 在CoreUserModel的变更方法中添加 NotificationCenter.default.post(name: NSNotification.Name("CoreUserUpdated"), object: self)
- 视图模型监听通知并更新UI属性:
// 在UserViewModel的init中添加 NotificationCenter.default.addObserver( forName: NSNotification.Name("CoreUserUpdated"), object: coreModel, queue: .main ) { [weak self] _ in guard let self = self else { return } self.displayName = self.coreModel.name self.avatarURL = self.coreModel.avatarURL } // 记得在视图模型销毁时移除观察者 deinit { NotificationCenter.default.removeObserver(self) }
关键设计要点
- 单一数据源落地:核心模型作为唯一真实数据来源,所有数据修改必须通过核心模型的方法完成,视图模型只是核心数据的“UI镜像”
- 分层隔离:核心模型负责业务逻辑和数据存储(可跨平台复用),视图模型负责UI适配和操作转发(SwiftUI专属),UI只关注展示和用户交互
- 自动刷新机制:利用SwiftUI的
ObservableObject+@Published特性,让UI自动响应数据变更,无需手动触发刷新
内容的提问来源于stack exchange,提问作者Oscar

