This is Understanding Combine, written by Matt Neuburg. Corrections and suggestions are greatly appreciated (you can comment here). So are donations; please consider keeping me going by funding this work at http://www.paypal.me/mattneub. Or buy my books: the current (and final) editions are iOS 15 Programming Fundamentals with Swift and Programming iOS 14. Thank you!
.map
(Publishers.Map) takes a function (the map function) that accepts a value published from upstream, and produces a new value, which may be of some other type. The new value is what is sent downstream; in effect, it replaces the upstream value.
.map
is frequently used to focus the incoming published value down to the part of that value you’re really after:
URLSession.shared.dataTaskPublisher(for: url!)
.map {$0.data}
The upstream URLSession.DataTaskPublisher produces a tuple of type (data: Data, response: URLResponse)
. It happens that we are not interested in the response
part, so we strip it, leaving the data
part, which is a Data object that proceeds downstream.
If the map function is of any complexity — anything more than a simple return statement — you might have to provide an in
line stating the output type. For example, this won’t compile:
let pub = URLSession.shared.dataTaskPublisher(for: url!)
.map {
let resp = $0.response
if (resp as? HTTPURLResponse)?.statusCode != 200 {
return Data()
}
return $0.data
}
You know that the map function produces a Data object, but the compiler takes no chances; it stops you with an error message. In the map function, you have to state the output type explicitly, and in order to do that, you have to give the map function’s incoming parameter a name — you can’t use $0
any more:
let pub = URLSession.shared.dataTaskPublisher(for: url!)
.map { tuple -> Data in
let resp = tuple.response
if (resp as? HTTPURLResponse)?.statusCode != 200 {
return Data()
}
return tuple.data
}
That’s a common thing to have to do with operators that take functions, so get used to it.
A variant on .map
is .tryMap
(Publishers.TryMap). It gives the map function the opportunity to throw an error; if it does throw, this operator cancels the upstream and passes the error down the pipeline as a failure. The produced error type does not have to be the same as the error type arriving from upstream; in fact, it is typed simply as Error. Again, that’s a common fact about operators with try
in their name.
So tryMap
is an opportunity to change downstream’s error type as well as its value type:
enum MyError : Error { case oops }
let pub = URLSession.shared.dataTaskPublisher(for: url!)
.tryMap { tuple -> Data in
let resp = tuple.response
if (resp as? HTTPURLResponse)?.statusCode != 200 {
throw MyError.oops
}
return tuple.data
}
In that example, the upstream publisher’s error type is URLError, but if the TryMap throws, it throws a MyError. This is not a problem for the downstream, which expects an Error of some sort. And it isn’t a problem for the TryMap itself; if the publisher throws a URLError, the TryMap passes it on down the pipeline and the map function doesn’t run. Even if the upstream failure type is Never, you can use TryMap to produce an error.
On the other hand, if you want to characterize the produced error as being of some specific type, you’ll need another operator. For example, here the data task publisher throws a URLError; and let’s suppose your tryMap
also throws a URLError (if it throws). Then you could follow the .tryMap
operator with a .mapError
operator that force-downcasts the Error to a URLError:
.mapError{$0 as! URLError}
That causes the downstream to expect a URLError. But of course that’s a forced downcast, so you’d better not be lying or you’ll crash.