Combine 在 SwiftUI 中的使用是天作之合。SwiftUI 的整个设计理念就是响应式,而 Combine 正是 Apple 为 Swift 生态提供的官方响应式编程框架。它们协同工作,为构建现代、声明式的 UI 提供了强大的支持。
核心用途:驱动数据流和状态更新
在 SwiftUI 中,Combine 主要被用于以下三个核心场景:
- 状态管理 :使用
@Published
包装属性,使其成为可观察的 Publisher。 - 生命周期与UI事件处理 :使用
.onReceive
修饰符监听外部事件。 - 异步操作处理:封装网络请求、定时器等异步任务。
SwiftUI 内置了对 Combine 的深度集成,你通常不需要手动调用 sink
和 store
,框架会自动帮你处理订阅和生命周期。
1. 状态管理:@Published
与 ObservableObject
这是 Combine 在 SwiftUI 中最常见、最重要的用法。它让你能够创建一个可观察的数据模型,当模型发生变化时,自动触发 UI 更新。
示例:创建一个可观察的 ViewModel
swift
import Combine
import SwiftUI
// 1. 让 class 遵循 ObservableObject 协议
class TimerViewModel: ObservableObject {
// 2. 使用 @Published 包装任何需要被观察的属性
// 当这些属性的值改变时,会发出事件,通知所有订阅的 View 更新。
@Published var currentTime: String = "00:00:00"
@Published var isRunning: Bool = false
private var timer: AnyCancellable?
private var startDate: Date?
func startTimer() {
isRunning = true
startDate = Date()
// 3. 使用 Combine 创建定时器 Publisher
timer = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect() // 自动连接
.sink { [weak self] _ in
guard let self = self, let startDate = self.startDate else { return }
// 计算时间差
let elapsed = Date().timeIntervalSince(startDate)
// 4. 更新 @Published 属性,触发 UI 更新
self.currentTime = self.formatTimeInterval(elapsed)
}
}
func stopTimer() {
isRunning = false
timer?.cancel() // 取消订阅,停止定时器
timer = nil
}
private func formatTimeInterval(_ interval: TimeInterval) -> String {
// ... 格式化时间的逻辑
let hours = Int(interval) / 3600
let minutes = Int(interval) / 60 % 60
let seconds = Int(interval) % 60
return String(format: "%02i:%02i:%02i", hours, minutes, seconds)
}
}
在 SwiftUI View 中使用
swift
struct TimerView: View {
// 4. 使用 @StateObject 或 @ObservedObject 来注入 ObservableObject
// SwiftUI 会自动订阅这个对象的 @Published 属性变化
@StateObject var viewModel = TimerViewModel()
var body: some View {
VStack {
Text(viewModel.currentTime) // 5. 直接使用 @Published 属性
.font(.largeTitle)
Button(action: {
if viewModel.isRunning {
viewModel.stopTimer()
} else {
viewModel.startTimer()
}
}) {
Text(viewModel.isRunning ? "Stop" : "Start")
.padding()
}
}
}
}
发生了什么?
@StateObject
创建并持有TimerViewModel
实例。- SwiftUI 自动订阅了
viewModel
的对象发布者 (即objectWillChange
Publisher)。 - 当任何一个
@Published
属性(currentTime
或isRunning
)发生变化时,viewModel
会发出事件。 - SwiftUI 收到事件后,会重新计算
body
属性,从而更新 UI。 - 你永远不需要手动 调用
viewModel.objectWillChange.sink {...}
,SwiftUI 帮你完成了所有订阅和管理工作。
2. 监听外部事件:.onReceive
修饰符
当你需要监听一个外部的 Publisher (不是 @Published
属性),并对它的值做出反应时,使用 .onReceive
修饰符。
示例:监听系统时间或通知
swift
struct ContentView: View {
@State private var systemTime: String = ""
// 创建一个定时器 Publisher
private let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
var body: some View {
Text("System Time: \(systemTime)")
.font(.title)
.onReceive(timerPublisher) { date in
// 这个闭包每秒都会在主线程被调用一次
let formatter = DateFormatter()
formatter.timeStyle = .medium
systemTime = formatter.string(from: date)
}
}
}
3. 处理异步任务:网络请求、数据库查询
将异步操作封装成 Combine Publisher,然后在 View 的初始化或 .task
修饰符中启动它。
示例:在 ViewModel 中发起网络请求
swift
class UserProfileViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var error: Error?
private var cancellables = Set<AnyCancellable>()
func fetchUser(userId: String) {
isLoading = true
error = nil
// 1. 创建网络请求 Publisher
let url = URL(string: "https://api.example.com/users/\(userId)")!
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main) // 2. 确保回到主线程更新 UI
.sink { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.error = error
}
} receiveValue: { [weak self] user in
self?.user = user // 3. 更新 @Published 属性,触发 UI 更新
}
.store(in: &cancellables) // 4. 必须存储订阅!
}
}
swift
struct UserProfileView: View {
@StateObject var viewModel = UserProfileViewModel()
let userId: String
var body: some View {
VStack {
if viewModel.isLoading {
ProgressView()
} else if let error = viewModel.error {
Text("Error: \(error.localizedDescription)")
} else if let user = viewModel.user {
Text("Hello, \(user.name)!")
}
}
.onAppear {
// 5. 在视图出现时触发网络请求
viewModel.fetchUser(userId: userId)
}
}
}
最佳实践总结
- 使用
ObservableObject
+@Published
:这是管理状态和驱动 UI 更新的首选方式。 - 使用
@StateObject
用于创建 ,@ObservedObject
用于从父视图传递。 - 在
ViewModel
中处理逻辑:将异步操作、业务逻辑放在 ViewModel 中,保持 View 的简洁。 - 妥善管理订阅 :在 ViewModel 中使用
Set<AnyCancellable>
来存储所有订阅,确保它们在 ViewModel 销毁时被自动取消。 - 线程安全 :使用
.receive(on:)
操作符确保在收到数据后切换回主线程 再更新@Published
属性。 - 使用
.onReceive
处理特殊事件 :用于监听那些不适合或无法用@Published
属性表示的外部事件流。
Combine 和 SwiftUI 的组合,使得数据流变得清晰、直接且易于维护:数据从 ViewModel 的 @Published
属性流出,自动驱动 SwiftUI View 的更新。你几乎不需要手动处理订阅的生命周期,框架为你搞定了一切。