Search code examples
iosswiftreactive-programmingcombine

Combine Subscriber not receiving values immediately


I am trying to come up with a Combine pipeline that does the following

  • Make an API call with certain pageIndex and send the results to subscriber
  • Increment the page Index and repeat the above until backend says there is nothing more to fetch

Here is what I have done so far

struct APIResponse {
  var hasNextPage: Bool = false
  var nextPageIndex: Int = -1
}
class Tester {
    func getAllDetails() -> AnyPublisher<APIResponse, Never> {
        // Subject holding the page index to be fetched
        let subject = CurrentValueSubject<Int, Never>(0)
        
        return subject.flatMap ({ index in
            return self.getDetails(index: index)
        })
        .handleEvents(receiveOutput: { response in
            if response.hasNextPage {
                subject.send(response.nextPageIndex)
            } else {
                subject.send(completion: .finished)
            }
        })
 // Ignore the call, Just did it please the compiler
        .replaceError(with: APIResponse())
        .eraseToAnyPublisher()
    }
    
    func getDetails(index: Int) -> AnyPublisher<APIResponse,MockError> {
        Future { promise in
            // Mocking API Response here
            if index < 5 {
                promise(.success(APIResponse(hasNextPage: true, nextPageIndex: index+1)))
            } else if index == 5 {
                promise(.success(APIResponse(hasNextPage: false)))
            } else {
                promise(.failure(MockError()))
            }
        }
        .eraseToAnyPublisher()
    }
}

let tester =  Tester()
tester.getAllDetails()
    .sink { _ in
        print("completed")
    } receiveValue: { numbers in
        print(numbers.debugDescription)
    }

The pipeline is working but it delivering all the results to subscriber at the end and not as they arrive. How do I change this to let subscriber receive intermediate values


Solution

  • There isn't anything wrong with your pipeline. Your problem is that your mock API call isn't realistic; A real API call will be asynchronous. Once you introduce asynchronous behaviour into your mock, you will get the result you expect:

    func getDetails(index: Int) -> AnyPublisher<APIResponse,MockError> {
        Future { promise in
            DispatchQueue.global(qos:.utility).asyncAfter(deadline: .now()+0.5) {
                    // Mocking API Response here
                if index < 5 {
                    promise(.success(APIResponse(hasNextPage: true, nextPageIndex: index+1)))
                } else if index == 5 {
                    promise(.success(APIResponse(hasNextPage: false)))
                } else {
                    promise(.failure(MockError()))
                }
            }
        }
        .eraseToAnyPublisher()
    }
    
    APIResponse(hasNextPage: true, nextPageIndex: 1)
    APIResponse(hasNextPage: true, nextPageIndex: 2)
    APIResponse(hasNextPage: true, nextPageIndex: 3)
    APIResponse(hasNextPage: true, nextPageIndex: 4)
    APIResponse(hasNextPage: true, nextPageIndex: 5)
    APIResponse(hasNextPage: false, nextPageIndex: -1)
    completed
    

    You will also need to ensure that your subscriber is retained:

    var cancellables = Set<AnyCancellable>()
    
    let tester =  Tester()
    tester.getAllDetails()
        .sink { _ in
            print("completed")
        } receiveValue: { numbers in
            print(numbers)
        }.store(in: &cancellables)