Search code examples
iosswiftrx-swiftgoogle-places-autocomplete

RxSwift: Combine two network request to return one value


I have two network requests that I would like to combine the results in one array of Object.

enum ResultType<T> {
  case success(T)
  case failure(Error)
}

static func requestPlaceSearch(query: String) -> Observable<ResultType<SearchResult>> {
    let urlString = "https://maps.googleapis.com/maps/api/place/autocomplete/json"
    let params = ["input": query, "types": "geocode",
                "key": "key"]
    return placeRequest(url: urlString, params: params)
}

static func requestPlace(with placeId: String) -> Observable<ResultType<Place>> {
    let params = ["placeid": placeId, "key": "key"]
    let urlString = "https://maps.googleapis.com/maps/api/place/details/json"
    return placeRequest(url: urlString, params: params)
}

requestPlaceSearch returns and observable of SearchResult I want to loop through SearchResult.predictions, get the id of each prediction make a request to requestPlace get the Place an append to array of Places

I want my final array to be

let places = Observable<[Place]> 

or

let places = [Place]()

Solution

  • I'm going to give you the answer, but I'm going to draw it out by explaining how I came up with the answer. Hopefully, you can emulate the process and be able to come up with the answer yourself in the next attempt.

    Start with:

    let foo = requestPlaceSearch(query: "")
    

    And look at foo's type which is: Observable<ResultType<SearchResult>>. Okay from there we need to extract the search result from the result type. So...

    let foo = requestPlaceSearch(query: "")
        .map { (result: ResultType<SearchResult>) -> SearchResult? in
            if case let .success(search) = result {
                return search
            } else {
                return nil
            }
        }
    

    Now what is foo's type? Observable<SearchResult?>. So lets filter out the optionals.

    let foo = requestPlaceSearch(query: "")
        .map { (result: ResultType<SearchResult>) -> SearchResult? in
            if case let .success(search) = result {
                return search
            } else {
                return nil
            }
        }
        .filter { $0 != nil }.map { $0! }
    

    Now foo is of type: Observable<SearchResult> So we can pull out the predictions.

    let foo = requestPlaceSearch(query: "")
        .map { (result: ResultType<SearchResult>) -> SearchResult? in
            if case let .success(search) = result {
                return search
            } else {
                return nil
            }
        }
        .filter { $0 != nil }.map { $0! }
        .map { $0.predictions }
    

    We are going to need that function to extract the success object again so let's move that into a separate function:

    func extractSuccess<T>(_ result: ResultType<T>) -> T? {
        if case let .success(search) = result {
            return search
        } else {
            return nil
        }
    }
    

    I'm assuming that predictions are just strings. You might have to extract the string IDs from it. So now foo is of type Observable<[String]>. Now that we have the ids we can call the other endpoint.

    let foo = requestPlaceSearch(query: "")
        .map(extractSuccess)
        .filter { $0 != nil }.map { $0! }
        .map { $0.predictions }
        .map { $0.map(requestPlace) }
    

    Now foo is of type Observable<[Observable<ResultType<Place>>]>. Next let's turn the [Observable] into a Observable<[T]>.

    let foo = requestPlaceSearch(query: "")
        .map(extractSuccess)
        .filter { $0 != nil }.map { $0! }
        .map { $0.predictions }
        .map { Observable.combineLatest($0.map(requestPlace)) }
    

    Which makes foo a Observable<Observable<[ResultType<Place>]>>. An Observable> can be flattened out pretty easy.

    let foo = requestPlaceSearch(query: "")
        .map(extractSuccess)
        .filter { $0 != nil }.map { $0! }
        .map { $0.predictions }
        .flatMap { Observable.combineLatest($0.map(requestPlace)) }
    

    That turns foo into an Observable<[ResultType<Place>]>

    let foo = requestPlaceSearch(query: "")
        .map(extractSuccess)
        .filter { $0 != nil }.map { $0! }
        .map { $0.predictions }
        .flatMap { Observable.combineLatest($0.map {
            requestPlace(with: $0)
                .map(extractSuccess)
                .filter { $0 != nil }.map { $0! }
            })
        }
    

    The part I added in the above is the same as the last one that returns a result type. Also, at this point foo is of type Observable<[Place]> and we're done.

    Note that this code doesn't deal with any of the failure results. I'll leave that as an exercise for the reader. :-)