Ensure Abstraction in your iOS Application Codebase when using Combine

Hide your Implementation Details to make your Code cleaner and safer

César Vargas Casaseca
Axel Springer Tech
6 min readOct 12, 2020

--

Photo by Daria Nepriakhina on Unsplash

Our iOS Team at WELT got recently very good news, we could set our App iOS Deployment Target to 13.0. That means not only that we should not support old versions anymore, but also that we can start using the cool APIs that were introduced in that version, especially SwiftUI and Combine.

With Combine we can write functional reactive code to process values over time through a declarative Swift API. It can be compared to other takes on this approach for Swift, such as RxSwift and ReactiveSwift.

Tip: If your app still cannot fully support Combine because the minimum deployment target is lower than iOS 13.0 don’t worry, you can start migrating it by using OpenCombine. According to their docs:
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple’s operating systems before macOS 10.15 and iOS 13, as well as Linux and Windows.

That way, given that their API is exactly the same as Combine, it will ease the migration process to Combine in the future; ideally you will just have to replaceimport OpenCombine with import Combine.

Coming back to Combine, for now two concepts are essential for those familiar with the RxSwift syntax or similar:

  • Observables are Publishers in Combine. They expose values that can change.
  • Observers are Subscribers in Combine. They subscribe to receive all these updates.

With this in mind, let’s see one example where we can use it.

Combine and MVVM

Combine, as any other reactive framework, is especially adequate to implement a MVVM architecture: the View Model encapsulates the processing and exposes the UI data, that can change over time. The View subscribes to the View Model to receive these updates and react accordingly, refreshing the UI and showing the new values to the user. Together with all the chaining operators that the reactive frameworks provide, it makes the code more declarative and thus readable.

In this example, we want to show the user a sport event result. As it changes over time, reactive programming is ideal for our case.

The first version of our ResultViewModel with Combine would be something like this:

Our View Model listens to the result updates from the fetcher implementing the delegate method, used for simplicity sake in our example (we could very well be using reactive programming in this layer as well). Once a new result has arrived, we use a PassthroughSubject that broadcasts new data to downstream subscribers, in this case the view, so it can accordingly refresh the UI:

Here, we subscribe to the View Model updates on the result, react refreshing the label when a new value is received always on the main thread, and store the AnyCancellable object in a Set, so we do not lose the reference.

Abstraction!

Photo by Jr Korpa on Unsplash

The example above works fine, we are implementing the business logic as required: every time the result changes it is reflected in the UI. But correct as it is, the code is still very faulty.

Why? because with PassthroughSubject we are exposing the implementation details of our operation. That is bad by reason of generating a dependance between the client, in this case the View, and the implementation, making it easy to break by any change or modification in the latter. Imagine for instance that the View Model needs to keep a buffer of the most recently published element for tracking purposes. In that case we cannot use the PassthroughSubject, but CurrentValueSubject instead. The View Model API will change.

But more importantly, with PassthroughSubject, we give the power to the view to send events through it, something that is clearly not its task. It should just listen, not update it. Our code isn’t safe.

This is when AnyPublisher comes on the scene. According to the Apple docs:

Use AnyPublisher to wrap a publisher whose type has details you don’t want to expose across API boundaries, such as different modules. Wrapping a Subject with AnyPublisher also prevents callers from accessing its send(_:) method. When you use type erasure this way, you can change the underlying publisher implementation over time without affecting existing clients.

This is exactly what we want:

  • Hide type details
  • Prevent callers from accessing send(_:)
  • Be able to change the publisher implementation over time without affecting clients

Oh, but wait, we can still push it a little bit forward into the Abstraction Realm right? We can create a protocol for the View Model, thus hiding more details (the Data Fetcher for instance) and enhancing testability by replacing it easily with a mock when unit testing it. For more info about Protocol-Oriented Programming in Swift, check this link.

Consequently, we can already implement our new version of the View Model:

This way we are just exposing an AnyPublisher object, so the clients can subscribe but not update as intended. With eraseToAnyPublisher() we achieve exactly that, to expose an instance of AnyPublisher to the downstream subscriber rather than this publisher’s actual type, an action that we call Type Erasure: we hide implementation details by exposing a more abstract type, a technique that can be applied thanks to polymorphism. Now the View has the right access level to the Publisher.

Another Turn on The Screw?

As Henry Miller in his superb novella, you might still want to push it a little bit forward. Why not using Publisher instead of AnyPublisher? Isn’t it even more abstract? At least it sounds so …

The answer is no, Publisher and AnyPublisher are different things, with different purposes: with Publisher we cannot achieve Type Erasure as with AnyPublisher. The former is a protocol with associated types, therefore it can be used when you define a function that has generics as part of the definition, such as a Protocol Extension, e.g. when creating a custom Combine operator. An example from here:

extension Publisher {
public func compactMapEach<T, U>(_ transform: @escaping (T) -> U?)
-> Publishers.Map<Self, [U]>
where Output == [T]
{
return map { $0.compactMap(transform) }
}
}

Photo by 🇨🇭 Claudio Schwarz | @purzlbaum on Unsplash

Conclusion

Now that iOS 14 is released, it is more likely that you will be able to drop older iOS version, thus being able to migrate to (or start using) Combine. When doing that, we should as always be conscious of writing a readable, maintainable and safe code that uses Abstraction when possible. Apple goes in the same direction, and adds Type Erasure to our toolset. There is no excuse to avoid ensuring Abstraction when writing your implementation with Combine.

To recapitulate, in this article we have seen:

  • How we can ease the future reactive migration process to Combine by using OpenCombine with earlier iOS versions
  • How we can use Combine to implement a simple MVVM solution
  • How we can support Abstraction and Protocol-Oriented Programming with Combine
  • Why we cannot use Publisher in the place of AnyPublisher

I would like to thank my colleague Ivan Lisovyi for his splendid insight and contribution about this topic, without which this story would not have been possible. As always, if you have a question or contribution please drop a message below.

Happy Abstraction!

--

--

César Vargas Casaseca
Axel Springer Tech

Senior iOS Developer at Automattic. Allegedly a clean pragmatic one. Yes, sometimes I do backend. Yes, in Kotlin, Javascript or even Swift.