Combine/Swift并发中RxSwift Signal的等效实现方案咨询
Combine/Swift并发中RxSwift Signal的等效实现方案咨询
我太懂你这种痛点了!之前用RxSwift的时候,Signal+PublishRelay这套组合简直是触发一次性UI动作(比如弹窗、Toast)的神器——不管你发多少次相同的内容,订阅者都会每次都响应,完全不用管状态维护,发完就忘。换到SwiftUI的状态驱动模式后,用@Published的话,因为只有值变化才会触发更新,相同内容发第二次就没反应了,确实很别扭。
下面给你两种最贴合需求的方案,分别对应Combine和纯Swift并发(Async/Await)的场景:
方案一:Combine + PassthroughSubject(对应Rx的PublishRelay+Signal)
Combine里的PassthroughSubject几乎就是PublishRelay的孪生兄弟——它只负责发送事件,不会保留最新值,而且不管内容是否重复,每次调用send()都会让订阅者收到事件,完美匹配你要的“事件驱动、发完即忘”的需求。
改造后的ViewModel
@MainActor final class MainViewModel: ObservableObject { @Published var items: [Item] = [] // 内部用PassthroughSubject发送事件,对应Rx的PublishRelay private let showAlertSubject = PassthroughSubject<String, Never>() // 对外暴露只读的AnyPublisher,避免外部随意发送事件 var showAlert: AnyPublisher<String, Never> { showAlertSubject.eraseToAnyPublisher() } let getAllItem: () async -> Result<[Item], Error> func fetchAll() { Task { let result = await getAllItem() switch result { case .success(let items): self.items = items case .failure: // 发送弹窗事件,哪怕内容和上次一样也会触发 showAlertSubject.send("Failed to fetch items...") } } } }
View层的用法
在View里我们用onReceive订阅这个Publisher,然后用一个本地的@State变量来控制弹窗的显示——这样每次收到事件,不管内容重复与否,都会更新状态并弹出Toast:
struct MainView: View { @ObservedObject private var viewModel: MainViewModel // 本地维护弹窗的临时状态,弹窗关闭时手动清空即可 @State private var currentAlertText: String? var body: some View { ZStack { // 你的原有界面内容... if let text = currentAlertText { SimpleToast( text: text, bgColor: .red, textColor: .white ) .onTapGesture { // 关闭弹窗时清空状态 currentAlertText = nil } } } // 订阅弹窗事件,每次收到就更新本地状态 .onReceive(viewModel.showAlert) { alertText in currentAlertText = alertText } } }
方案二:纯Swift并发(Async/Await)+ AsyncStream
如果不想用Combine,纯Swift并发里的AsyncStream也能实现同样的效果——它可以创建一个异步的事件流,每次调用yield()发送事件,View层通过迭代这个流来响应。
改造后的ViewModel
@MainActor final class MainViewModel: ObservableObject { @Published var items: [Item] = [] // 用AsyncStream的Continuation来发送事件 private var showAlertContinuation: AsyncStream<String>.Continuation? // 对外暴露AsyncStream,供View层迭代监听 var showAlert: AsyncStream<String> { AsyncStream { continuation in showAlertContinuation = continuation // 防止内存泄漏,流终止时清空引用 continuation.onTermination = { [weak self] _ in self?.showAlertContinuation = nil } } } let getAllItem: () async -> Result<[Item], Error> func fetchAll() { Task { let result = await getAllItem() switch result { case .success(let items): self.items = items case .failure: // 发送弹窗事件 showAlertContinuation?.yield("Failed to fetch items...") } } } }
View层的用法
用.task修饰符来启动一个异步任务,迭代监听AsyncStream的事件:
struct MainView: View { @ObservedObject private var viewModel: MainViewModel @State private var currentAlertText: String? var body: some View { ZStack { // 你的原有界面内容... if let text = currentAlertText { SimpleToast( text: text, bgColor: .red, textColor: .white ) .onTapGesture { currentAlertText = nil } } } // 异步监听事件流,每次yield都会触发 .task { for await alertText in viewModel.showAlert { currentAlertText = alertText } } } }
为什么不推荐用延时重置@Published的方案?
你之前说的“延时几秒把showAlert设为nil”确实能凑合用,但有几个明显的问题:
- 逻辑不优雅:为了触发状态更新,强行引入了不必要的状态重置逻辑,把事件驱动的需求硬生生改成了状态驱动;
- 可能出现冲突:如果短时间内多次触发错误(比如用户连续点刷新),延时重置可能会导致弹窗提前消失,或者新的弹窗被旧的重置操作覆盖;
- 可读性差:其他开发者看代码的时候,会疑惑为什么要加这么一段延时逻辑,增加了维护成本。
而上面的两种方案都是纯事件驱动,完全贴合你“触发弹窗就忘”的需求,逻辑更清晰,也更符合SwiftUI/Combine/Swift并发的设计理念。
内容来源于stack exchange




