ObservableObject + MVVM + Repository 完整指南
适用版本:iOS 14+ | 核心技术:SwiftUI + Combine + 架构模式
目录
- [ObservableObject 核心详解](#ObservableObject 核心详解)
- 核心配套组件
- 手动/自动触发视图刷新
- 状态管理修饰符对比
- 完整实战工程模板
- 最佳实践与常见误区
- [iOS17+ 新方案 @Observable](#iOS17+ 新方案 @Observable)
- [进阶:Repository 仓储模式](#进阶:Repository 仓储模式)
- 总结
1. ObservableObject 核心详解
1.1 定义与作用
ObservableObject 是 Combine 框架 提供的核心协议,用于让类类型 成为可观察对象。其核心能力是:当对象的属性发生变化时,自动向订阅者(主要是 SwiftUI 视图)发送变化通知,触发 UI 刷新。
1.2 核心特性
- 仅适用于类(值类型不支持)
- 自带
objectWillChange发布器,用于广播变化信号 - 是 SwiftUI 中管理复杂引用类型状态、跨视图共享状态的核心方案
- 常与
@Published、@StateObject配合实现完整的状态管理
2. 核心配套组件
2.1 @Published
- 类型:Combine 框架的属性包装器
- 作用:标记需要监听的属性,自动触发
objectWillChange发送变化通知 - 语法:
@Published var 属性名: 类型 = 初始值 - 优势:简化开发,无需手动编写通知逻辑
2.2 objectWillChange
- 类型:
ObservableObject协议自带的发布器 - 作用:手动发送 变化通知,替代
@Published的自动行为 - 适用场景:自定义属性变化的触发时机、复杂逻辑场景
2.3 @StateObject (iOS14+)
- 类型:SwiftUI 属性包装器
- 作用:创建并持有
ObservableObject实例,管理其生命周期 - 核心特点:视图重建时不会重新初始化对象,是视图内可观察对象的单数据源
- 推荐场景:视图内部创建 ViewModel 时使用
2.4 @ObservedObject
- 类型:SwiftUI 属性包装器(iOS13+)
- 作用:订阅外部传递 的
ObservableObject,仅观察状态变化 - 核心特点:不管理对象生命周期,仅接收外部状态
- 推荐场景:子视图接收父视图传递的 ViewModel
2.5 @EnvironmentObject
- 类型:SwiftUI 属性包装器
- 作用:全局环境共享
ObservableObject,跨多层级视图传递状态 - 优势:无需手动逐层传参,简化跨视图状态共享
3. 手动/自动触发视图刷新
3.1 自动刷新(@Published)
最常用的方式,通过属性包装器自动管理变化通知。
swift
import Combine
class CounterViewModel: ObservableObject {
// 标记需要监听的属性,自动触发刷新
@Published var count: Int = 0
func increment() {
count += 1
}
}
3.2 手动刷新(objectWillChange)
适用于需要自定义刷新时机的场景,手动发送通知。
swift
class ManualCounterViewModel: ObservableObject {
var count: Int = 0
func increment() {
count += 1
// 手动发送变化通知
objectWillChange.send()
}
// 通过属性观察器触发
var username: String = "" {
didSet {
objectWillChange.send()
}
}
}
4. 状态管理修饰符对比
| 修饰符 | 生命周期管理 | 适用场景 | 版本要求 |
|---|---|---|---|
@State |
视图内持有 | 值类型简单状态(Int/String) | 全版本 |
@StateObject |
视图内创建/持有 | 视图内创建可观察对象(ViewModel) | iOS14+ |
@ObservedObject |
外部持有 | 接收父视图传递的可观察对象 | iOS13+ |
@EnvironmentObject |
全局环境共享 | 跨层级视图共享状态 | iOS13+ |
5. 完整实战工程模板
可直接复制运行(Xcode SwiftUI 项目)
swift
import SwiftUI
import Combine
// ==============================================
// 核心 ViewModel:遵循 ObservableObject 协议
// ==============================================
class AppViewModel: ObservableObject {
// 自动触发视图刷新的属性
@Published var userName: String = "SwiftUI 开发者"
@Published var count: Int = 0
// 手动触发刷新的属性
var manualCount: Int = 0
// 业务逻辑:自动计数+1
func increment() {
count += 1
}
// 业务逻辑:手动计数+1
func manualIncrement() {
manualCount += 1
objectWillChange.send()
}
// 业务逻辑:重置所有状态
func resetAll() {
userName = "SwiftUI 开发者"
count = 0
manualCount = 0
}
}
// ==============================================
// 主视图:@StateObject 管理 ViewModel 生命周期
// ==============================================
struct ContentView: View {
@StateObject private var vm = AppViewModel()
var body: some View {
NavigationStack {
VStack(spacing: 25) {
// 状态展示
Text("用户名:\(vm.userName)").font(.title)
Text("自动计数:\(vm.count)").font(.title2)
Text("手动计数:\(vm.manualCount)").font(.title2)
// 操作按钮
Button("自动计数 +1") { vm.increment() }
Button("手动计数 +1") { vm.manualIncrement() }
Button("修改用户名") { vm.userName = "ObservableObject 高手" }
// 导航子视图
NavigationLink("跳转子视图") {
SubView(viewModel: vm)
}
// 导航全局环境视图
NavigationLink("全局环境视图") {
EnvironmentView()
}
Button("重置所有状态") { vm.resetAll() }
.foregroundColor(.red)
}
.padding()
.navigationTitle("ObservableObject 实战")
}
// 全局注入 ViewModel
.environmentObject(vm)
}
}
// ==============================================
// 子视图:@ObservedObject 接收传递的 ViewModel
// ==============================================
struct SubView: View {
@ObservedObject var viewModel: AppViewModel
var body: some View {
VStack(spacing: 20) {
Text("子视图 - 计数:\(viewModel.count)").font(.title)
Button("子视图 +1") { viewModel.increment() }
Button("子视图重置") { viewModel.resetAll() }
}
.padding()
.navigationTitle("子视图")
}
}
// ==============================================
// 全局环境视图:@EnvironmentObject 共享状态
// ==============================================
struct EnvironmentView: View {
@EnvironmentObject var viewModel: AppViewModel
var body: some View {
VStack(spacing: 20) {
Text("全局环境 - 用户名:\(viewModel.userName)").font(.title)
Text("全局计数:\(viewModel.count)").font(.title2)
Button("全局修改用户名") { viewModel.userName = "全局状态共享成功" }
}
.padding()
.navigationTitle("全局环境")
}
}
// 预览代码
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
6. 最佳实践与常见误区
6.1 最佳实践
- 优先使用 @StateObject :视图内创建可观察对象时,必须使用
@StateObject,避免状态丢失。 - 分层设计:ViewModel 封装业务逻辑,视图仅负责 UI 展示,不处理业务代码。
- 全局状态用 EnvironmentObject:跨多层级视图共享状态时,简化传参逻辑。
- 线程安全 :所有 UI 相关操作必须在主线程执行,异步逻辑更新属性后切换主线程。
- 避免循环引用 :ViewModel 中引用外部对象时,使用
weak/unowned修饰。
6.2 常见误区
- 用
@ObservedObject在视图内创建对象 → 视图重建导致状态丢失、对象重复初始化。 - 忘记添加
@Published标记属性 → 视图不会自动刷新。 - 子线程直接更新属性 → 触发 UI 线程安全异常。
- 混淆值类型和引用类型:值类型用
@State,引用类型用@StateObject/@ObservedObject。
7. iOS17+ 新方案 @Observable
苹果在 iOS 17+ 推出了简化的状态管理方案,替代传统的 ObservableObject,核心优势是减少样板代码:
- 无需遵循
ObservableObject协议 - 无需使用
@Published包装器 - 自动监听所有属性变化,语法更简洁
代码示例
swift
import SwiftUI
// 无需遵循协议,无需 @Published
@Observable
class NewCounterViewModel {
var count: Int = 0
func increment() {
count += 1
}
}
// 视图使用
struct NewCounterView: View {
// 直接使用,无需额外包装
@State private var viewModel = NewCounterViewModel()
var body: some View {
VStack {
Text("计数:\(viewModel.count)")
Button("+1") { viewModel.increment() }
}
}
}
8. 进阶:Repository 仓储模式
8.1 核心概念
Repository(仓储模式) 是 MVVM 架构的数据层核心 ,作为 ViewModel 与数据来源(网络、数据库、缓存)之间的中间层,统一封装所有数据操作,实现解耦。
8.2 架构分层
View(视图) → ViewModel(ObservableObject) → Repository(数据仓储) → 数据来源(API/数据库/缓存)
8.3 核心优势
- 解耦:ViewModel 不关心数据来源,仅调用仓储接口。
- 易维护:切换数据源(如替换网络库、升级数据库)无需修改 ViewModel。
- 易测试:可快速编写模拟数据(Mock),方便单元测试。
- 单一职责:数据操作全部收敛到仓储层,代码结构更清晰。
8.4 完整实战代码
swift
import SwiftUI
import Combine
// 1. 数据模型
struct User: Identifiable {
let id = UUID()
var name: String
var count: Int
}
// 2. Repository 协议(面向接口编程)
protocol UserRepositoryProtocol {
func fetchUser() -> User
func updateUserName(_ newName: String)
func incrementCount()
func resetData()
}
// 3. 仓储实现(本地数据,可替换为网络请求)
class UserRepository: UserRepositoryProtocol {
private var user = User(name: "SwiftUI 开发者", count: 0)
func fetchUser() -> User { return user }
func updateUserName(_ newName: String) { user.name = newName }
func incrementCount() { user.count += 1 }
func resetData() { user = User(name: "SwiftUI 开发者", count: 0) }
}
// 4. ViewModel(依赖仓储协议,不依赖具体实现)
class UserViewModel: ObservableObject {
private let repository: UserRepositoryProtocol
@Published var user: User
// 依赖注入:初始化时传入仓储
init(repository: UserRepositoryProtocol = UserRepository()) {
self.repository = repository
self.user = repository.fetchUser()
}
// 业务逻辑:调用仓储方法
func updateName(_ newName: String) {
repository.updateUserName(newName)
user = repository.fetchUser()
}
func addCount() {
repository.incrementCount()
user = repository.fetchUser()
}
func reset() {
repository.resetData()
user = repository.fetchUser()
}
}
// 5. 视图(仅展示 UI)
struct RepositoryDemoView: View {
@StateObject private var vm = UserViewModel()
var body: some View {
VStack(spacing: 30) {
Text("用户名:\(vm.user.name)").font(.title)
Text("计数:\(vm.user.count)").font(.title2)
Button("修改用户名") { vm.updateName("Repository 架构师") }
Button("计数 +1") { vm.addCount() }
Button("重置") { vm.reset() }
}
.padding()
.navigationTitle("MVVM + Repository")
}
}
// 预览
struct RepositoryDemoView_Previews: PreviewProvider {
static var previews: some View {
RepositoryDemoView()
}
}
9. 总结
- ObservableObject 是 SwiftUI 响应式状态管理的核心协议,搭配
@Published实现自动状态刷新。 - @StateObject 是视图内创建可观察对象的首选,负责生命周期管理;@ObservedObject 用于接收外部状态。
- Repository 模式 是 MVVM 架构的数据层最佳实践,实现 ViewModel 与数据来源的解耦,提升代码可维护性和测试性。
- iOS 17+ 推出的
@Observable宏简化了状态管理代码,是未来的推荐方案。 - 遵循分层设计 和单一职责原则,是构建大型 SwiftUI 项目的核心原则。