iosswiftgraphqlcombine

iOS Handle multiple asynchronous callbacks using Combine


I’m encountering an issue with my in-memory caching implementation where fetchValue is not capturing the second callback. My current setup involves fetching data using Apollo GraphQL with Combine's Future, but the second callback is not being triggered as expected.

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)
    }
}

==============

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))
            }
        }
    }
}

output:

print("API>> hit")
 print("API>> response")
 print("API>> API>> response")
 print("API>> response")

Expected output

print("API>> hit")
 print("API>> response")
 print("API>> API>> response")
 print("API>> response")
 print("API>> API>> response")

The second callback from fetchValue is not being captured from myRequest. I’m expecting to see both API responses in the output, but the second one is missing. It seems like Combine Future is only support first callback not the second one, but I’m not sure how to properly handle it.

Why is fetchValue not capturing the second callback? How can I ensure that both callbacks are properly captured and handled? Any help or suggestions on how to resolve this issue would be greatly appreciated!


Solution

  • Your code is intimately tied to libraries (the Apollo Client) that that folks answering your question may not be familiar with, and a test environment would require a GraphQL server that they don't have - so answering your question directly will be difficult.

    Within the sample you've given, the function myRequest calls fetchValue. fetchValue returns a Future and myRequest seems to wrap that Future up in another Future that really doesn't serve a purpose apart from echoing the results of 'fetchValue`.

    I don't understand why you've done that. It really seems that myRequest and fetchValue are equivalent - you should be able to remove myRequest altogether and simply call fetchValue.

    You imply that apolloClient.fetch may call its completion callback function more than once for a given query. Is that the case?

    If so then as suggested in the comments, you may want to use a PassthroughSubject (representing the return type as an AnyPublisher).

    A Future represents a single request that returns a single result at some time in the future. It is a stream that emits one value and then terminates. So it would not be a good model if apolloClient.fetch is going to return more than one result.

    A PassthroughSubject is a generic stream that can carry multiple values. It will terminate when an error is encountered or when the stream is explicitly finished. (It's not clear from your code how to tell when apolloClient.fetch is done sending values and the stream should terminate)

    Ignoring that detail, what you may need is something like this:

    private func fetchValue<T, Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy) -> AnyPublisher<Response<T>, CYGErrorType>? where T: RootSelectionSet {
    
      let resultSubject = 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 {
              resultSubject.send(completion: .failure(CYGErrorType.default))
              return
            }
    
            let response = Response(value: data, response: nil, error: nil)
            resultSubject.send(response)
            print("API>> response")
    
    
          case .failure(let error):
            resultSubject.send(completion: .failure(error))
          }
        case .failure(let error):
          resultSubject.send(completion: .failure(error))
        }
      }
    
      return resultSubject.eraseToAnyPublisher()
    }
    

    Here the code creates a PassthroughSubject and each time the completion handler is called it will emit the value passed to the callback through that subject. At the bottom of the function we convert the subject to an AnyPublisher since the fact that it's a PassthroughSubject is an implementation detail that folks outside of this function don't need to know