Home > Back-end >  Combine: Chain requests with dependency, keep both responses
Combine: Chain requests with dependency, keep both responses

Time:02-27

I'm trying to wrap my head around this call in Combine.

I have two models and calls. One is an array of place data, the second an array for the OpenWeather response. What I need is to pass the latitude and longitude from my first call response into the second call. At the same time I need to keep the response for both calls.

Bear in mind that this is my first chained request.

enum callAPI {
static let agent = Agent()
static let url1 = URL(string: "**jsonURL**")!
static let url2 = URL(string: "https://api.openweathermap.org/data/2.5/weather?lat=\(latitude)&lon=\(longitude)&APPID=**APIkey**&unites=metric")! }

extension callAPI {
    
static func places() -> AnyPublisher<[Place], Error> {
    return run(URLRequest(url: url1))
}

static func weather(latitude: Double, longitude: Double) -> AnyPublisher<[ResponseBody], Error> {
    return run(URLRequest(url: url2))
}

static func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<T, Error> {
    return agent.run(request)
        .map(\.value)
        .eraseToAnyPublisher()
}}

func chain() {
let places = callAPI.places()
let firstPlace = places.compactMap { $0.first }
let weather = firstPlace.flatMap { place in
    callAPI.weather(latitude: place.latitude, longitude: place.longitude)
}

let token = weather.sink(receiveCompletion: { _ in },
                        receiveValue: { print($0) })

RunLoop.main.run(until: Date(timeIntervalSinceNow: 10))

withExtendedLifetime(token, {})}

This is the model:

struct Place: Decodable, Identifiable {
let id: Int
let name: String
let description: String
let latitude, longitude: Double
let imageURL: String }

struct ResponseBody: Decodable {
var coord: CoordinatesResponse
var weather: [WeatherResponse]
var main: MainResponse
var name: String
var wind: WindResponse

    struct CoordinatesResponse: Decodable {
    var lon: Double
    var lat: Double
}

    struct WeatherResponse: Decodable {
    var id: Double
    var main: String
    var description: String
    var icon: String
}

    struct MainResponse: Decodable {
    var temp: Double
    var feels_like: Double
    var temp_min: Double
    var temp_max: Double
    var pressure: Double
    var humidity: Double
}

    struct WindResponse: Decodable {
    var speed: Double
    var deg: Double
}}

extension ResponseBody.MainResponse {
var feelsLike: Double { return feels_like }
var tempMin: Double { return temp_min }
var tempMax: Double { return temp_max }}

CodePudding user response:

You are not going to be able to chain the requests the way you are trying to and still capture all the results.

Think of it this way. By chaining Combine operators you're constructing a pipeline. You can decide what to put into the input to the pipeline, and you can dump whatever comes out of the output of the pipeline into a sink where you can see the results, but you can't go through the sides of the pipe to see the intermediate values (at least not without cutting a window in the pipe which we'll get to).

Here's your code:

let places = callAPI.places()
let firstPlace = places.compactMap { $0.first }
let weather = firstPlace.flatMap { place in
    callAPI.weather(latitude: place.latitude, longitude: place.longitude)
}

let token = weather.sink(receiveCompletion: { _ in },
                        receiveValue: { print($0) })

Those variables each held a piece of the pipeline (not the values that will flow through the pipe) and you're screwing the pipeline together putting longer and longer pieces in each variable.

If I want to make the whole pipeline a bit more obvious write it like this:

let cancellable = callAPI.places()
   .compactMap { $0.first }
   .flatMap { place in
    callAPI.weather(latitude: place.latitude, longitude: place.longitude)
   }
   .sink(receiveCompletion: { _ in },
         receiveValue: { print($0) })

(note that might not compile as is... I pulled it together in the answer editor)

When you chain the operators directly it's obvious that you don't have any opportunity to catch intermediate results. For your pipeline the stuff that goes into the pipeline comes from the network. You catch the stuff flowing out of pipeline in a sink. But notice how you only get to look at the "stuff" flowing through the pipeline in closures that are part of the pipeline itself.

Now, if you really want to cut a window into the pipeline to pull out intermediate stuff, you need one of those closures that can push the value out of the pipeline. In this case, to get at the array of Places you might do it using handleEvents. It would look something like this:

var allPlaces : [Place]?

callAPI.places()
    .handleEvents(receiveOutput: { allPlaces = $0 })
    .compactMap { $0.first }
    ...

In this code, you catch the receiveOutput event and sneak the result out into a nearby variable.

handleEvents, in my opinion, is one of those "Great Power, Great Responsibility" operators. In this case it will let you do what you are asking to do, but I'm not sure you should do it.

The whole point of chaining operators together is that the resulting pipeline "should" be free of side-effects. In this case handleEvents is being used to explicitly introduce a side-effect (setting the allPlaces variable). Essentially this is, in my opinion, a code smell that suggests you may need to rethink your design.

  • Related