Combine 不仅是一套处理异步事件的工具箱,更是驱动整个应用数据流动的发动机。将 Combine 与合适的架构模式结合,可以显著提升代码的可测试性、可维护性和扩展性。本章将深入探讨 Combine 如何与 MVVM、Repository、状态机、MVI、Redux 以及依赖注入等主流架构模式无缝协作,帮助你构建出一套健壮、清晰且易于演进的 SwiftUI 应用骨架。
1. 架构模式概述
在 Combine 的世界里,数据流是单向的、可预测的。无论你采用何种上层架构,其底层本质都是相同的:State → View → Action → Reducer → New State。Combine 负责将状态变化发布出去,SwiftUI 视图自动响应这些变化。
本章将覆盖以下核心架构模式及其与 Combine 的集成:
- MVVM (Model-View-ViewModel):最基础的 SwiftUI 架构,将视图状态与业务逻辑分离。
- Repository 模式:抽象数据源,实现缓存策略与远程数据的无缝切换。
- 状态机 (State Machine):严格定义有限状态及转换条件,避免非法状态。
- MVI (Model-View-Intent):强调意图与单向数据流,适合复杂交互。
- Redux:全局单一 Store + 纯函数 Reducer,极高可预测性。
- 依赖注入 (DI):解耦组件,提升测试性。
2. MVVM + Combine:基石架构
MVVM 是 SwiftUI 应用的默认架构选择。Combine 在其中扮演着"粘合剂"的角色,让 ViewModel 的状态变化自动驱动 View 更新。

sql
用户操作 → View 调用 ViewModel → ViewModel 暴露 @Published 属性
→ ViewModel 调用 Service / Repository
→ Service 返回 Publisher 给 ViewModel
→ ViewModel 更新 @Published,View 自动刷新
2.1 输入输出分离的 ViewModel
swift
class LoginViewModel: ObservableObject {
// 输入
@Published var email = ""
@Published var password = ""
// 输出
@Published var isLoginEnabled = false
@Published var isLoading = false
@Published var errorMessage: String?
@Published var isLoggedIn = false
private let authService: AuthServiceProtocol
private var cancellables = Set<AnyCancellable>()
init(authService: AuthServiceProtocol = AuthService.shared) {
self.authService = authService
setupBindings()
}
private func setupBindings() {
Publishers.CombineLatest($email, $password)
.map { email, password in
!email.isEmpty && email.contains("@") && password.count >= 6
}
.assign(to: \.isLoginEnabled, on: self)
.store(in: &cancellables)
}
func login() {
isLoading = true
errorMessage = nil
authService.login(email: email, password: password)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
}, receiveValue: { [weak self] user in
self?.isLoggedIn = true
})
.store(in: &cancellables)
}
}
ViewModel 只负责将业务逻辑转化为视图可消费的状态,View 则完全被动地渲染这些状态:
swift
struct LoginView: View {
@StateObject private var viewModel = LoginViewModel()
var body: some View {
VStack(spacing: 20) {
TextField("邮箱", text: $viewModel.email)
SecureField("密码", text: $viewModel.password)
if let error = viewModel.errorMessage {
Text(error).foregroundColor(.red)
}
Button(action: viewModel.login) {
if viewModel.isLoading { ProgressView() }
else { Text("登录") }
}
.disabled(!viewModel.isLoginEnabled)
}
.padding()
}
}
3. Repository 模式:抽象数据源
Repository 是数据层的抽象,让 ViewModel 不用关心数据来自远程 API、本地缓存还是 Mock。Combine 的 Publisher 使缓存策略可以优雅实现。
swift
protocol UserRepositoryProtocol {
func fetchUser(id: String) -> AnyPublisher<User, Error>
func updateUser(_ user: User) -> AnyPublisher<Void, Error>
}
class UserRepository: UserRepositoryProtocol {
private let remote: RemoteAPIProtocol
private let cache: LocalCacheProtocol
func fetchUser(id: String) -> AnyPublisher<User, Error> {
// 先从缓存取,同时请求网络更新
let cached = cache.fetchUser(id: id).catch { _ in Empty() }
let remote = remote.fetchUser(id: id)
.handleEvents(receiveOutput: { [weak self] user in
try? self?.cache.saveUser(user)
})
return cached.merge(with: remote).eraseToAnyPublisher()
}
}
ViewModel 依赖 Repository 协议,可在测试时注入 Mock。
4. 状态机模式:消除非法状态
当页面状态复杂时(如 idle → loading → loaded / error),使用枚举定义状态机可以保证状态的唯一性与合法性,避免组合爆炸。
swift
enum LoadableState<T: Equatable>: Equatable {
case idle
case loading
case loaded(T)
case error(Error)
static func == (lhs: LoadableState, rhs: LoadableState) -> Bool {
switch (lhs, rhs) {
case (.idle, .idle), (.loading, .loading):
return true
case (.loaded(let l), .loaded(let r)):
return l == r
case (.error, .error):
return true
default: return false
}
}
}
class ItemsViewModel: ObservableObject {
@Published private(set) var state = LoadableState<[Item]>.idle
private let repo: ItemRepositoryProtocol
private var bag = Set<AnyCancellable>()
func load() {
state = .loading
repo.fetchItems()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
if case .failure(let error) = completion {
self?.state = .error(error)
}
}, receiveValue: { [weak self] items in
self?.state = .loaded(items)
})
.store(in: &bag)
}
}
视图中使用 switch state 渲染不同界面,所有状态均被覆盖。
5. MVI:意图与副作用管理
MVI (Model-View-Intent) 进一步约束了状态变更的唯一入口:通过 Intent(意图)表达用户操作,通过 Effect 管理副作用。Combine 的 PassthroughSubject 非常适合作为 Intent 通道。
swift
enum ViewState { case idle, loading, loaded(User), error(Error) }
enum Intent { case loadUser(id: Int), reload }
class MVIViewModel: ObservableObject {
@Published private(set) var state = ViewState.idle
private let intents = PassthroughSubject<Intent, Never>()
private var bag = Set<AnyCancellable>()
init() {
intents.sink { [weak self] intent in
switch intent {
case .loadUser(let id): self?.loadUser(id: id)
case .reload: /* 根据当前状态重载 */
}
}.store(in: &bag)
}
func send(_ intent: Intent) { intents.send(intent) }
private func loadUser(id: Int) {
state = .loading
// 网络请求...
}
}
View 只需要调用 viewModel.send(.loadUser(id: 1)),所有状态变更都流经单向管道。
6. Redux:全局单一 Store
Redux 将整个应用的状态集中到单一的 Store 中,通过 Reducer 纯函数生成新状态。Combine 在这里充当中间件的角色,处理异步 Action 流。
swift
struct AppState { var user: User?; var items: [Item] = [] }
enum Action { case loadUser(id: Int); case userLoaded(User); case userFailed(Error) }
typealias Reducer = (inout AppState, Action) -> AnyPublisher<Action, Never>?
func appReducer(state: inout AppState, action: Action) -> AnyPublisher<Action, Never>? {
switch action {
case .loadUser(let id):
state.isLoading = true
return fetchUser(id: id)
.map { Action.userLoaded($0) }
.catch { Just(Action.userFailed($0)) }
.eraseToAnyPublisher()
case .userLoaded(let user):
state.user = user; return nil
case .userFailed(let error):
state.errorMessage = error.localizedDescription; return nil
}
}
class Store: ObservableObject {
@Published var state = AppState()
private let reducer: Reducer; private var bag = Set<AnyCancellable>()
func dispatch(_ action: Action) {
if let effect = reducer(&state, action) {
effect.sink { self.dispatch($0) }.store(in: &bag)
}
}
}
Redux 的优势在于严格的状态可预测性与时间旅行调试能力,适合大型复杂应用。
7. 依赖注入:架构的润滑剂
无论是 ViewModel 依赖 Service,还是 Service 依赖 API,通过协议进行依赖注入是保证测试性与灵活性的关键。
swift
protocol NetworkServiceProtocol { ... }
protocol CacheServiceProtocol { ... }
class DIContainer {
static let shared = DIContainer()
lazy var networkService: NetworkServiceProtocol = NetworkService()
lazy var cacheService: CacheServiceProtocol = CacheService()
lazy var userRepo: UserRepositoryProtocol = UserRepository(remote: networkService, cache: cacheService)
func makeLoginViewModel() -> LoginViewModel {
LoginViewModel(authService: authService)
}
}
在 SwiftUI 中,@EnvironmentObject 可以传递容器,但更推荐在 View 初始化时手动注入 ViewModel,以保持显式依赖。
8. 架构选择指南与混合实践
| 架构 | 复杂度 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|---|
| MVVM | 低 | 简单到中等 | 上手快,SwiftUI 原生支持 | 状态可能分散 |
| Repository | 中 | 需缓存/多数据源 | 数据层分离清晰 | 增加一层抽象 |
| State Machine | 中 | 页面状态多、复杂 | 消除非法状态 | 需维护枚举 |
| MVI | 中高 | 交互密集、副作用多 | 单向流,意图清晰 | 概念较多 |
| Redux | 高 | 大型应用、多人协作 | 单一数据源,可预测 | 样板代码多 |
实际项目中往往混合使用:以 MVVM 为基础,View 的复杂状态用 State Machine 管理,数据层采用 Repository 模式,跨组件通信借助 Redux 或 @EnvironmentObject。依赖注入则贯穿始终。
9. 测试的收益
上述架构都高度依赖协议和依赖注入,这使得 Combine 的 ViewModel、Service、Reducer 都能被轻松测试。使用 XCTestExpectation 等待异步流的完成,或使用 ImmediateScheduler 消除时间依赖。
swift
func testViewModelState() {
let mockService = MockAuthService()
let vm = LoginViewModel(authService: mockService)
vm.email = "test@test.com"
vm.password = "123456"
vm.login()
let exp = XCTestExpectation()
vm.$isLoggedIn.sink { loggedIn in
if loggedIn { exp.fulfill() }
}.store(in: &bag)
wait(for: [exp], timeout: 1)
}
10. 总结
Combine 是响应式架构的天然搭档。无论你选择 MVVM、MVI、Redux 还是自定义状态机,核心思想都是将异步数据流的管理 从 View 中剥离,交由 Combine 的 Publisher/Subscriber 体系处理。本章覆盖了与 Combine 结合最紧密的几种架构模式及其典型实现,并给出了混合使用的建议。最终目标是构建出层次清晰、职责单一、易于测试的 SwiftUI 应用。