You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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”确实能凑合用,但有几个明显的问题:

  1. 逻辑不优雅:为了触发状态更新,强行引入了不必要的状态重置逻辑,把事件驱动的需求硬生生改成了状态驱动;
  2. 可能出现冲突:如果短时间内多次触发错误(比如用户连续点刷新),延时重置可能会导致弹窗提前消失,或者新的弹窗被旧的重置操作覆盖;
  3. 可读性差:其他开发者看代码的时候,会疑惑为什么要加这么一段延时逻辑,增加了维护成本。

而上面的两种方案都是纯事件驱动,完全贴合你“触发弹窗就忘”的需求,逻辑更清晰,也更符合SwiftUI/Combine/Swift并发的设计理念。

内容来源于stack exchange

火山引擎 最新活动