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

iOS使用Combine处理多异步回调:fetchValue未捕获第二次回调

内存缓存结合Apollo GraphQL+Combine的回调捕获问题

问题背景

在实现内存缓存时,使用Apollo GraphQL结合Combine的Future获取数据,遇到fetchValue的第二次回调无法被myRequest捕获的问题,第二次回调未触发预期的输出。

相关代码

myRequest方法

func myRequest<T, Q: GraphQLQuery>(query: Q) -> Future<Response<T>, CYGErrorType>? where T: RootSelectionSet {
    return Future<Response<T>, CYGErrorType> { promise in
        guard let futureFetchValue = self.fetchValue(query: query, cachePolicy: self.model.cachePolicy) as Future<Response<T>, CYGErrorType>? else {
            promise(.failure(.default))
            return
        }
        
        futureFetchValue
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    break
                case .failure(let error):
                    promise(.failure(error))
                }
            }, receiveValue: { result in
                print("API>> API>> response")
                promise(.success(result))
            })
            .store(in: &self.cancellable)
    }
}

fetchValue方法

private func fetchValue<T, Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy) -> Future<Response<T>, CYGErrorType>? where T: RootSelectionSet {
    return Future<Response<T>, CYGErrorType> { promise in
        print("API>> hit")
        apolloClient.fetch(query: query, cachePolicy: cachePolicy) { result in
            switch result {
            case .success(let graphQLResult):
               
                let validateResponse = graphQLResult.isValid(query: query)
                switch validateResponse {
                case .success:
                    guard let data = graphQLResult.data as? T else {
                        promise(.failure(CYGErrorType.default))
                        return
                    }
                    
                    let response = Response(value: data, response: nil, error: nil)
                    promise(.success(response))
                    print("API>> response")

                    
                case .failureWithError(let error):
                    promise(.failure(error))
                }
            case .failure(let error):
                let cygError = self.graphQLErrorParsing(error: error, queryName: Query.operationName)
                promise(.failure(cygError))
            }
        }
    }
}

当前输出

API>> hit
API>> response
API>> API>> response
API>> response

预期输出

API>> hit
API>> response
API>> API>> response
API>> response
API>> API>> response

问题解答

1. 为什么fetchValue无法捕获第二次回调?

核心原因是Combine的Future是一次性的:Future的promise只能被调用一次,一旦调用了promise(.success)或promise(.failure),这个Future就进入完成状态,不会再响应后续的任何事件。

当Apollo的fetch回调第二次触发时,fetchValue里的Future已经因为第一次调用promise(.success)结束,第二次调用promise不会产生任何效果,自然无法被myRequest里的sink捕获到。

另外,myRequest中使用单个AnyCancellable存储订阅,每次调用myRequest都会覆盖之前的订阅,导致旧订阅被取消,这也会影响第二次回调的捕获。

2. 如何确保两次回调都被正确捕获和处理?

要支持多回调事件,不能用Future,需改用PassthroughSubject或CurrentValueSubject这类支持多值输出的Publisher类型,具体改造步骤如下:

步骤1:修改fetchValue返回PassthroughSubject

private func fetchValue<T, Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy) -> PassthroughSubject<Response<T>, CYGErrorType>? where T: RootSelectionSet {
    let subject = PassthroughSubject<Response<T>, CYGErrorType>()
    print("API>> hit")
    apolloClient.fetch(query: query, cachePolicy: cachePolicy) { result in
        switch result {
        case .success(let graphQLResult):
            let validateResponse = graphQLResult.isValid(query: query)
            switch validateResponse {
            case .success:
                guard let data = graphQLResult.data as? T else {
                    subject.send(completion: .failure(CYGErrorType.default))
                    return
                }
                let response = Response(value: data, response: nil, error: nil)
                subject.send(response) // 每次有数据就发送事件
                print("API>> response")
            case .failureWithError(let error):
                subject.send(completion: .failure(error))
            }
        case .failure(let error):
            let cygError = self.graphQLErrorParsing(error: error, queryName: Query.operationName)
            subject.send(completion: .failure(cygError))
        }
    }
    return subject
}

步骤2:修改myRequest并调整订阅存储方式

首先将类中的单个cancellable改为Set类型,避免覆盖订阅:

private var cancellables = Set<AnyCancellable>()

然后修改myRequest方法:

func myRequest<T, Q: GraphQLQuery>(query: Q) -> PassthroughSubject<Response<T>, CYGErrorType>? where T: RootSelectionSet {
    guard let subject = self.fetchValue(query: query, cachePolicy: self.model.cachePolicy) as PassthroughSubject<Response<T>, CYGErrorType>? else {
        let errorSubject = PassthroughSubject<Response<T>, CYGErrorType>()
        errorSubject.send(completion: .failure(.default))
        return errorSubject
    }
    
    subject
        .sink(receiveCompletion: { completion in
            switch completion {
            case .finished:
                break
            case .failure(let error):
                // 可在此处理全局错误逻辑
                break
            }
        }, receiveValue: { result in
            print("API>> API>> response")
        })
        .store(in: &self.cancellables) // 存入Set,保留所有订阅
    
    return subject
}

额外说明

  • PassthroughSubject:可以多次发送value事件,直到调用send(completion:)结束,完美匹配Apollo fetch可能多次回调的场景(比如缓存更新后再次返回数据)。
  • Set:存储订阅避免被覆盖,确保所有回调都能被处理。
  • 若需要保留最新数据供新订阅者获取,可改用CurrentValueSubject,它会保存当前最新值,新订阅者会立即收到该值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 03:22:36