You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift Combine:CurrentValueSubject发送值与完成事件异常排查

问题根源分析

你的问题出在两个核心点上:

  1. 复用已终止的CurrentValueSubject实例:cacheSubject是SSNetworkManager的成员变量,当你第一次处理缓存请求时调用cacheSubject.send(completion: .finished),这个Subject就会进入终止状态。后续所有基于它创建的Publisher,只会直接发送完成事件,再也无法传递任何数据。
  2. 事件发送与订阅的时机不匹配:你先把cancelable赋值为cacheSubject.eraseToAnyPublisher(),之后才发送数据和完成事件。如果调用方的订阅操作在这之后执行,即便CurrentValueSubject保留了最新值,只要Subject已经终止,订阅者也只能收到完成通知。
修复方案

我们需要为每个缓存请求创建独立的Subject实例,彻底避免复用已终止的Subject。推荐使用PassthroughSubject(缓存场景不需要初始值,它更贴合需求),以下是修改后的完整代码:

public class SSNetworkManager { 
    public static let shared = SSNetworkManager() 
    private var cache: [String: CachedData] = [:] 
    // 移除全局的cacheSubject,改为按需创建独立实例
    
    @discardableResult 
    public func makeServiceCall<D: Decodable>(forRequest request: SSNetworkRequest<D>, onMainThread: Bool = true) -> AnyPublisher<D, Error>? { 
        guard let urlRequest = request.urlRequest else { return nil } 
        var cancelable: AnyPublisher<Data, Error> 
        
        if let url = urlRequest.url?.absoluteString, let cachedData = cache[url], cachedData.isValid { 
            // 为当前请求创建专属的PassthroughSubject
            let cacheSubject = PassthroughSubject<Data, Error>()
            cancelable = cacheSubject.eraseToAnyPublisher() 
            // 依次发送缓存数据和完成事件
            cacheSubject.send(cachedData.data) 
            cacheSubject.send(completion: .finished) 
        } else { 
            cancelable = URLSession.shared.dataTaskPublisher(for: urlRequest)
                .tryMap {[weak self] (data, response) -> Data in 
                    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { 
                        throw SSNetworkError(httpCode: (response as? HTTPURLResponse)?.statusCode ?? 0, data: data) 
                    } 
                    if request.shouldCacheNow, let url = urlRequest.url?.absoluteString { 
                        self?.cache[url] = CachedData(data: data, expirationTime: request.cacheExpirationTime) 
                    } 
                    return data 
                }
                .eraseToAnyPublisher() 
        } 
        
        // 统一处理解码和线程切换逻辑,提升可读性
        let decodedPublisher = cancelable
            .decode(type: D.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
        
        return onMainThread 
            ? decodedPublisher.receive(on: RunLoop.main).eraseToAnyPublisher() 
            : decodedPublisher
    } 
} 

fileprivate struct CachedData { 
    let data: Data 
    let expirationTime: Date 
    var isValid: Bool { 
        return Date().compare(expirationTime) != .orderedDescending 
    } 
}
关键改动说明
  • 取消全局Subject复用:每个缓存请求都创建新的PassthroughSubject,确保每个请求的事件流完全独立,不会被之前的请求终止操作影响。
  • 替换为PassthroughSubject:缓存场景下我们有明确的待发送数据,不需要CurrentValueSubject的初始值特性,用PassthroughSubject更简洁,也避免了初始空Data的干扰。
  • 优化代码结构:把解码和线程切换逻辑统一提取,让核心的缓存/网络请求逻辑更清晰。

这样修改后,调用方就能同时收到缓存数据和完成通知,且所有请求的事件流不会互相干扰。

内容的提问来源于stack exchange,提问作者Son Nguyen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:06:48