Combine在swiftUI中的使用

Combine 在 SwiftUI 中的使用是天作之合。SwiftUI 的整个设计理念就是响应式,而 Combine 正是 Apple 为 Swift 生态提供的官方响应式编程框架。它们协同工作,为构建现代、声明式的 UI 提供了强大的支持。

核心用途:驱动数据流和状态更新

在 SwiftUI 中,Combine 主要被用于以下三个核心场景:

  1. 状态管理 :使用 @Published 包装属性,使其成为可观察的 Publisher。
  2. 生命周期与UI事件处理 :使用 .onReceive 修饰符监听外部事件。
  3. 异步操作处理:封装网络请求、定时器等异步任务。

SwiftUI 内置了对 Combine 的深度集成,你通常不需要手动调用 sinkstore,框架会自动帮你处理订阅和生命周期。


1. 状态管理:@PublishedObservableObject

这是 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()
            }
        }
    }
}

发生了什么?

  1. @StateObject 创建并持有 TimerViewModel 实例。
  2. SwiftUI 自动订阅了 viewModel对象发布者 (即 objectWillChange Publisher)。
  3. 当任何一个 @Published 属性(currentTimeisRunning)发生变化时,viewModel 会发出事件。
  4. SwiftUI 收到事件后,会重新计算 body 属性,从而更新 UI。
  5. 永远不需要手动 调用 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)
        }
    }
}

最佳实践总结

  1. 使用 ObservableObject + @Published :这是管理状态和驱动 UI 更新的首选方式
  2. 使用 @StateObject 用于创建@ObservedObject 用于从父视图传递。
  3. ViewModel 中处理逻辑:将异步操作、业务逻辑放在 ViewModel 中,保持 View 的简洁。
  4. 妥善管理订阅 :在 ViewModel 中使用 Set<AnyCancellable> 来存储所有订阅,确保它们在 ViewModel 销毁时被自动取消。
  5. 线程安全 :使用 .receive(on:) 操作符确保在收到数据后切换回主线程 再更新 @Published 属性。
  6. 使用 .onReceive 处理特殊事件 :用于监听那些不适合或无法用 @Published 属性表示的外部事件流。

Combine 和 SwiftUI 的组合,使得数据流变得清晰、直接且易于维护:数据从 ViewModel 的 @Published 属性流出,自动驱动 SwiftUI View 的更新。你几乎不需要手动处理订阅的生命周期,框架为你搞定了一切。

相关推荐
我的写法有点潮5 小时前
Vue实例都做了什么?
前端·javascript·vue.js
写代码的stone5 小时前
如何基于react useEffect实现一个类似vue的watch功能
前端·javascript·面试
仙人掌一号5 小时前
Webpack打包流程简述——新手向
前端·javascript
用户47949283569155 小时前
面试官:你知道deepseek的ai生成代码预览是用什么做的吗?
前端·javascript·面试
六月的可乐5 小时前
AI助理前端UI组件-悬浮球组件
前端·人工智能
鹏多多5 小时前
vue3监听属性watch和watchEffect的详解
前端·javascript·vue.js
ruanCat5 小时前
使用 cloudflare worker 实现域名重定向
前端
华仔啊5 小时前
关于移动端100vh的坑和终极解决方案,看这一篇就够了!
前端·css
Hashan5 小时前
Webpack 核心双引擎:Loader 与 Plugin 详解
前端·webpack