Gregor
Gregor

Reputation: 95

Access used values in a combine-operator-chain

I wonder if it is possible to access values in a combine-operator-chain that are used further up in the chain.

For example, if you have an array of strings, and you download a resource with each string, is it possible to access the string itself?

Maybe a pseudo-code example will help understand better:

let strings = ["a", "b", "c"]

strings.publisher
    .compactMap{ str in
        URL(string: "https://someresource/\(str)")
    }
    .flatMap { url  in
        URLSession.shared.dataTaskPublisher(for: url)
    }
    .map { $0.data}
    .map { data in
        // here I would need the "str" string from above
    }

Help is much appreciated

Thx, Gregor

Upvotes: 2

Views: 741

Answers (1)

matt
matt

Reputation: 534950

The answer that Jake wrote and deleted is correct. I don't know why he deleted it; maybe he felt he couldn't support it with example code. But the answer is exactly right. If you need the initial str value later in the pipeline, it is up to you to keep passing it down through every step. You typically do that by passing a tuple of values at each stage, so that the string makes it far enough down the chain to be retrieved. This is a very common strategy in Combine programming.

For a simple example, take a look at the Combine code in the central section of this article:

https://www.biteinteractive.com/swift-5-5-asynchronous-looping-with-async-await/

As I say in the article:

You’ll observe that, as opposed to GCD where local variables just magically “fall through” to nested completion handlers at a lower level of scope, every step in a Combine pipeline must explicitly pass down all information that may be needed by a later step. This can result in some rather ungainly values working their way down the pipeline, often in the form of a tuple, as I’ve illustrated here.

But I don’t regard that as a problem. On the contrary, being explicit about what’s passing down the pipeline seems to me to be a gain in clarity.

To illustrate further, here's a rewrite of your pseudo-code; this is real code, you can run it and try it out:

class ViewController: UIViewController {
    var storage = Set<AnyCancellable>()
    override func viewDidLoad() {
        super.viewDidLoad()
        let strings = ["a", "b", "c"]
        let pipeline = strings.publisher
            .map { str -> (String, URL) in
                let url = URL(string: "https://www.apeth.com/pep/manny.jpg")!
                return (str, url)
            }
            .flatMap { (str, url) -> AnyPublisher<(String, (data: Data, response: URLResponse)), URLError> in
                let sess = URLSession.shared.dataTaskPublisher(for: url)
                    .eraseToAnyPublisher()
                let just = Just(str).setFailureType(to: URLError.self)
                    .eraseToAnyPublisher()
                let zip = just.zip(sess).eraseToAnyPublisher()
                return zip
            }
            .map { (str, result) -> (String, Data) in
                (str, result.data)
            }
            .sink { comp in print(comp) } receiveValue: { (str, data) in print (str, data) }
        pipeline.store(in: &storage)
    }
}

That's not the only way to express the pipeline, but it does work, and it should give you a starting point.

Upvotes: 6

Related Questions