ReactiveSwift vs. Combine: Mapping Strategies
Combine has been with us for quite some time – 2 years, since the launch of iOS 13. But we couldn’t start adopting it in our projects until now. Our policy for starting a new project is to support the last 3 versions of iOS. And since iOS 15 was released in September we can finally start to use Combine in our codebase.
Currently, ReactiveSwift is our go-to FRP framework. In comparison to Combine, it approaches reactive programming differently. ReactiveSwift provides objects and several complex operators that are not present in Combine. So naturally, our first question was "how can we write this in Combine”?
First thing that we use very often and that we found differs in these two frameworks is ReactiveSwift's flatMap(.latest)
operator.
The main difference between flatMap(.latest
in ReactiveSwift and flatMap
in Combine is how the mapped (inner) stream is handled. In ReactiveSwift, the completion from the mapped stream does not affect the outer stream and therefore when a new value is sent, a new inner stream is created. In Combine, when the inner stream completes, the whole stream also completes. This problem occurred in many places, the most painful one to find was when users were stuck on one screen and buttons did not trigger underlying API requests.
Our first approach to solve the problem was not to use flatMap
at all. We just used a double sink
instead. This solved the problem but it was a really ugly solution. Basically, this solution creates a side-effect inside a reactive flow. It also loses the ability to cancel the previous stream if a new outer value is sent before the inner stream sends a value. So no! This was not the correct way.
The second approach was better. Inside flatMap
, we handled the completion explicitly and returned Empty(completeImmediately: false)
for that case. This also solved the problem, it was much nicer and it worked – in like 99% of cases. Under some very rare circumstances (I cannot describe when and how) it just didn't work at all. Even though we returned Empty(completeImmediately: false)
, the whole stream closed itself. Very very weird...
But at the end we found the holy grail. Combine has a built-in operator switchToLatest()
which in combination with map
does exactly what we wanted.
To adopt it, we had to change all flatMap
to map
and after each map
the switchToLatest()
call had to be added. And that's it. All our streams started to work exactly as they should.
That was our first problem on the way to adopt Combine. ReactiveSwift has a lot more things that are not available in Combine, so let’s hope their implementation is not going to be as painful as finding the correct implementation of flatMap(.latest)
.