Combine: From zero to… Oh! I get it.(Part II)
This is a three part article that aims to explain what Combine is and how to use it.
In the previous section I went over the theory behind the framework and the main concepts that defines it. If you missed it and are curious about it, feel free to check it out and come back when ready.
After reading this part of the series, you will have a better understanding of the technical aspects behind each component and how they work individually.
To do so, let’s continue with the analogy of the cookie store and fit the different actors of the framework in it.
Note: The intention of the article is to serve as reference on how each component works separately. Think of it as a dictionary where you can go to easily understand what a component does.
In the next article we will make use of all the components in a simple project, and with that you should have enough information to be able to make use of the framework on your own.
In case you are new to the series, the cookie store is an analogy I used in the previous article to explain how the relationship between Publishers, Subscribers and Operators looked like. In this store there were two rooms, one in the back where the kitchen is, and the front of the shop where the customers come to place their orders.
Nothing critical, just imagine you are in a store that sells chocolate chip cookies and everything will make sense.
Publishers
As we already know. A publisher is the object responsible for delivering values over time to its subscribers.
Conveniently, Combine provides a set of publishers that we can use straight away for different situations. Meet the main actors of our story:
Let’s take a closer look at them from top to bottom:
Note: In the following code examples you will see “.subscriber” being chained to the publishers. This is not a real function nor object, but it helps to explain the distinct cases, so feel free to ignore this part for now, we will revisit it in the Subscribers part of this article.
Just
Full name:
Just<Output>
Just might be the simpler publisher within the framework. Upon subscription it will provide a single value and terminate.
Note: Just is so good at what it does that it will never fail. This publisher can’t produce an error.
Just is the type of worker we call a fixer. We give it one job, and that job gets done immediately. In our case, Just is the person in front of the store giving free samples to the people to get them to go inside and buy more. Its job looks like this:
- Stand outside with a tray of cookies. (Init step)
- A person passes by and notices Just. (Subscriber subscribes)
- Person asks for a sample (Subscriber Demands)
- Just provides the sample to the person. (Subscriber receives the value)
- Just say thanks to the customer. (Subscriber receives completion event)
Future
Full name:
Future<Output, Failure> where Failure: Error
Is not crazy to assume that at times we need to perform operations asynchronously, whatever their nature might be, and these are precisly the cases where we need to hire someone like Future.
In simple terms, Future is a publisher that will allow you to perform the operations you need before passing down the result to the subscriber.
At the store, whenever we need to cook a batch of cookies, Future is the one we go to as it’s in charge of the oven. It looks something like this:
- Preheat the oven and prepare work station. (Init step)
- Future grabs a new tray of raw cookies and puts them in the oven. (Publisher performs asynchronous task)
- A person in the counter receives a new order. (Subscriber subscribes)
- This person asks Future for a new batch. (Subscriber demands)
- Future provides a new batch of cookies, if any. (Subscriber receives the value)
- Future notifies that his job is done. (Subscriber receives completion event)
Now, I won’t lie to you, sometimes we do face complication at the kitchen, the oven has been with us for some time, so there are occasions when we have a malfunction from its part, and because Future is simple a cooker and not a technician, when this happens the counter wouldn’t receive the cookies but simply a notification from it informing of the malfunction:
- Future notifies that his job couldn’t be done because of an oven malfunction. (Subscriber receives failure event)
It might not be obvious from the example above, but if you look carefully you will notice that Future actually starts preparing the cookies immediately, it doesn’t wait for the counter to ask for a new batch. It tries to be ready as soon as possible in order to provide the cookies as fast as possible.
In combine terms this might be counter-intuitive as if we look at the Life Cycle and how other publishers work, then we will notice that usually the publisher waits for the subscriber to demand for values before performing any task. But as we can see, Future executes its closure as soon as its initialized and provides the result when the subscriber demands it.
So keep in mind that if you are attempting to read something from a disk, network, or somewhere else, Future will execute this task immediately.
Luckily, if we want to delay our task until we actually have someone to consume it, we have another employee in our store.
Deferred
Full name:
Deferred<DeferredPublisher> where DeferredPublisher: Publisher
As we saw above Future is always trying to be ahead of the game, but because we want to provide the best quality possible and only cook the cookies once we have a customer for them, we need to calm Future down and tell it when to do its job.
That’s why we hired Deferred, in our setup, you can think of it as the manager of Future, its responsibility is to keep an eye on Future and inform when it should carry out its tasks.
Let’s take a closer look:
- Deferred arrives at the kitchen and stands in between the counter and the oven. (Init step)
- A new order arrives to the counter. (Subscriber subscribes)
- The counter asks for a new batch. (Subscriber demands)
- Deferred notifies Future about the request. (Publisher executes closure and returns the wrapped publisher)
- Future performs its job. (Wrapped publisher executes its closure, if any)
- Future provides the new batch of cookies. (Subscriber receives the value)
- Future notifies that the job is done. (Subscriber receives completion event)
Note: In case that the wrapped publisher could provide a failure event, then the subscriber would need to take this into account. I’m omitting this flow for the simplicity of the article, but keep in mind that it can also happen.
Important: What Deferred it’s actually doing, is delaying the Instantiation of the wrapped publisher. This means that when a subscriber demands a value from it, what it will receive is the wrapped publisher, which will in turn provide the value that the subscriber is expecting.
As you can see, thanks to Deferred we are able to take advantage of the power to perform asynchronous tasks of Future without compromising resources before we are ready to actually make use of them.
Note: In this example we are using Future, but if you look at the signature of Deferred you will notice that DeferredPublisher is no more than a Publisher, which means that you can use it in combination with any other Publisher, not only Future.
Empty
Full name:
Empty<Output, Failure> where Failure: Error
Normally when you go to a food establishment, together with your order you will get some napkins, you know, to keep things clean. We try to do the same at the store, but our supply of napkins is limited and more often than not we ran out of them. When this happens, we don’t want to create a scene, so we simply dispatch our customer without them.
It looks something like this:
- Napkins supply is over. (Init step)
- A worker in the counter will dispatch order. (Subscriber subscribes)
- This worker ask for napkins to another. (Subscriber demands)
- The other worker nods with a smile and provides nothing. (Subscriber receives completion event)
Empty is useful in error handling scenarios where the value is an optional, or where you want to resolve an error by simply not sending anything. In this case, the napkins will not change the tasting experience of our customer, so we simply proceed without them.
Technically speaking. Empty is a publisher that will never send any value down the stream. It will by default immediately send a completion event, but this could also be changed to never send one. Ultimately creating a publisher that does absolutely nothing.
Fail
Full name:
Fail<Output, Failure> where Failure: Error
There are occasions when a customer comes and asks for a cookie that we don’t produce, in those cases we politely inform the customer that we don’t have what they ask.
- Employee at the counter notices that a customer wants a white cookie. (Init step)
- Customer arrives at the counter. (Subscriber subscribes)
- Customer asks for a white chocolate cookie. (Subscriber demands)
- Worker notifies that we don’t sell this type of cookie. (Subscriber receives failure event)
We pay specially well to this worker because of its communication skills. We don’t want to end up in a situation where a customer is disappointed or offended because we don’t sell what they ask for.
Jokes aside, Fail is the publisher you will use when something down your data stream didn’t go as expected and you want to provide a publisher that can provide a failure event and nothing else.
Make sure you hire enough Fail workers in your shop to gracefully manage any negative scenarios.
Record
Full name:
Record<Output, Failure> where Failure: Error
You might have noticed that working in our kitchen is not an easy task. That’s why we have hired a teacher called Record.
Record knows in advance all the steps needed to prepare our tasty cookies, and when we get a new apprentice its job is to share this knowledge. The way this works is as follows:
- Record was hired. (Init step)
- Record was taught the preparation process. (Publisher adds values to a buffer, including completion or failure event)
- New apprentice joins. (Subscriber subscribes)
- Apprentice shows eagerness to learn. (Subscriber demands)
- Once all the steps have been successfully executed, Record notifies that the apprentice is ready, or not. (Subscriber receives completion or failure event)
Think of this as having an array of values that you would like to provide to the subscribers in that specific order. The difference here being that instead of using an array, you are giving the opportunity to create it as part of the instantiation process of the Publisher.
Note: An alternative would be to use the Publisher Sequence.
Subjects
protocol Subject: AnyObject, Publisher
Ok, so now that we know what publishers are, the different options and how they work, we can talk about a special type of publisher, also known as Subjects.
Subject is no more than a protocol that allows a publisher to expose a method to “inject” values into the stream from an outside caller.
Let’s take a moment to understand what that means.
If we look at all the publishers above, we will notice that all of them have one single job, like this:
- We initialize them.
- They receive a subscriber.
- They inject the data they were initialized with or execute their closures.
- Send a completion or failure event that terminates them.
We have no control over how or when this data is going to be passed down stream, or even to delay the completion event.
This is one of the benefits of the Subjects. Because they expose their send(_:) method we can maintain a publisher “alive” to send more data to its subscribers until we decide it fulfilled its purpose.
It might not be super clear yet, so let’s have a look at the two built-in subjects in Combine within the context of our Cookie Store to help us understand the concept.
CurrentValueSubject
final class CurrentValueSubject<Output, Failure> where Failure: Error
Because our store is not that big we need to control the amount of people we allow inside simultaneously.
For this job we hired CurrentValueSubject. Its job is to keep track of how many people we have inside the store, and notify the guard at the front door so it knows if it should allow people to get in or if they should wait for a spot to become available. A regular day would look like this:
- CurrentValue arrives to the store and knows that we have a max capacity of 5 people. (Init step)
- The guard at the door says hi to CurrentValue. (Subscriber subscribes)
- The guard asks how many spots are available. (Subscriber demands)
- CurrentValue responds that currently we have 5 spots. (Subscriber receives value event)
- A person arrives to the store and enters. CurrentValue notices this and it informs the guard that now we only have 4 spots available (Subject updates its value and emits an event downstream)
- The guard acknowledges this new information. (Subscriber receives value event)
- This person then leaves the store. CurrentValue then proceeds to inform the guard that now we have 5 spots available. (Subject updates its value and emits an event downstream)
- The guard acknowledges this new information. (Subscriber receives value event)
CurrentValueSubject allows us to create a publisher that wraps a single value and emits a new event every time this value changes.
From the example above we can see that this special publisher starts behaving like a Just publisher, but because it doesn’t send a completion event and it exposes its send(_:) method we are able to keep notifying subscribers about the value changes overtime.
In the example above we are only using one subscriber, but in reality you could add as many as you needed, allowing you to propagate changes to a wider audience.
Important: Every time you add a new subscriber, because they are the ones asking for the data, the subject will immediately respond with the LATEST updated value.
In the example above, if another subscriber would have subscribed when the value was 4, then that would have been the data of the first value event, and not the 5 that the other subscriber received.
This subject wraps a SINGLE value, it doesn’t contain a buffer to check previous values.
Once you the subject has fulfilled its purpose, or something went wrong. You can also make use of the send method to pass down a completion or failure event. Just keep in mind that after doing this, no other events would be emitted.
PassthroughSubject
final class PassthroughSubject<Output, Failure> where Failure: Error
Passthrough is our dispatch person, its job is to inform the customers once their order is ready. As its name my suggest, it doesn’t know much, it only receives a nudge from the kitchen when an order is ready and then repeats this information for the waiting customers to hear.
The process would look like this:
- Passthrough arrives to the store and positions itself at the counter. (Init step)
- A customer makes a purchase and moves to the waiting area. (Subscriber subscribes)
- The customer asks if the cookies are ready. (Subscriber demands)
- No response from Passthrough. ( 🦗 )
- A new batch of cookies is ready to be dispatched and Passthrough informs the waiting customers. (Subject emits a value event downstream)
- The waiting customer hears this. (Subscriber receives value event)
As you can see PassthroughSubject is a publisher that broadcasts data to downstream subscribers. Unlike CurrentValueSubject, it doesn’t have an initial value nor keeps track of the latest updates.
Same as with CurrentValueSubject, once it has fulfilled its purpose, or something went wrong. You can also make use of the send method to pass down a completion or failure event. And again, keep in mind that after doing this no other events would be emitted.
Subscribers
The time has come, we can finally talk about this demanding object, the one that is at the other side of the data stream.
From the theory article we learned that this object is the one responsible of requesting and digesting all the events that a publisher emits.
The same as with the publisher Combine provides us with two built in subscribers:
- Sink
- Assign
Let’s take a closer look at them:
Sink
final class Sink<Input, Failure> where Failure: Error
In our store, one clear example of a subscriber would be our customers. Once it places an order it gets ready to receive two things before leaving: first the cookies, and secondly the receipt of the order.
Sink is the versatile form of the built in subscribers. It allows you to provide two closures: one to receive the values, and one to receive the completion or failure event from the publisher.
So in our example above, the cookies would arrive from the first closure and the receipt would come in the form of a completion event without an error.
Let’s break it down:
- Customer arrives (Subscriber subscribes)
- Customer places order (Subscriber demands)
- Employee provides cookies (Publisher emits value event)
- Customer takes cookies from the employee (Sink executes receiveValue closure)
- Employee gives the receipt of the purchase (Publisher emits completion event)
- Customer takes the receipt from the employee (Sink executes the receiveCompletion closure)
Notice in the code snippet above that the completion event comes in the form of an enum value, either as finished when everything went ok, or as failure with an optional attached value.
Assign
final class Assign<Root, Input>
The second built in subscriber that we have available is Assign.
This subscriber allows us to update a property on a KVO compliant object.
In our store context, Assign would be the person keeping track of the sales of the day. Yeah, you know how important data is nowadays.
Every time we dispatch an order, Assign goes to a white board and updates the number in it with the new total of sales.
- Assign takes place next to the white board looking at the cashier. (Subscriber subscribes)
- Assign asks for the number of sales until now. (Subscriber demands)
- The cashier informs Assign of a new sale. (Assign assigns the new value to provided key path on the given object)
For as long as the upstream remains valid, Assign will update the property on the given object indicated by the key path.
You might be wondering what happens if an error is passed down the stream. Well that’s a good question as Assign expects all errors to be handled before it’s invoked. You will notice that if you try to hook Assign to a Publisher with a Failure type different from Never, you will get a compile time error.
Important: The Assign instance created by this operator maintains a strong reference to the object, and sets it to nil when the upstream publisher completes (either normally or with an error).
Cancellable
protocol Cancellable
&
final class AnyCancellable
I promised that I would explain to you how this class works and why it was important to keep in mind when talking about memory management.
Working on a cookie store is not super clean, but because we handle food we need to do our best to keep it under control. Well, when working with combine the same happens.
Every time you make a connection between Publisher and Subscriber you are leaving behind some crumbs in the form of subscriptions, and we need to be able to clean them up whenever we are ready to do so. The way Combine allows us to do so is by asking the Subscribers to conform to Cancellable.
Cancellable is a protocol that indicates that an activity or action supports cancellation.
The idea behind this is that when the method cancel() is executed on the subscriber, it terminates the subscription between the objects and stops processing the data stream
Now, the problem with this is that we would need to keep a reference to every subscriber we create in order to clean it up at a later point, and by doing so we are potentially giving access to external parties to execute the methods from the subscriber like requesting for more data for example.
To avoid exposing more than we need, but still allowing us to control our subscriptions, Combine provides a type-erased reference that converts any subscriber to the type AnyCancellable, allowing the use of .cancel() on that reference, but not access to the subscription itself.
Take a moment to go back to the examples of Sink and Assign and notice that both functions return an instance of AnyCancellable
The Cancellable protocol provides a convenience method to store the type-erased instance in a specified collection. This way we can keep all our references in once place without extra effort.
This way of storing the references is quite handy since, as you will notice in the second example, once these references are deallocated the subscriptions will also be terminated as if we would have called cancel() on each one individually.
Operators
Every time we get a new intern in the kitchen we see the same reaction in their face when they see all the tools we use. The same happens when we start working with Combine and we see the huge list of operators that the framework made available to us.
The answer is the same, don’t stress, it will make sense when it’s supposed to.
Rather than showing you how to use each single operator I’d rather focus on three important concepts, so let’s get to it:
Operators Composition
As I mentioned in the previous article, an operator is nothing more than a convenience function that will allow us to react upon the data in the stream as either a Publisher or a Subscriber.
In practice this means that we can do some work with the data before we are ready to digest it with a final subscriber.
The first thing you will notice from the example above is that we went from a starting publisher type to a different final one. The first one was a Sequence Publisher and the last one is a simple Just.
Now, what really is happening here is that, every time we chained an operator, a new publisher was create. This newly created publisher is making use of the data from the previous step to execute its action.
Let’s break it down in steps to see it clearly:
- We created a Publisher.Sequence that will emit value events for each integer from 0 to 10.
- We used these outputs to create a Publisher.Filter that would apply a predicate over the values.
- Then, after filtering the odd integers, we proceeded to create a Publisher.Count.
- Finally, and just to show that we can also change the data type, we mapped the result of the previous publisher into a String.
- Which is then received by a subscriber as we already know.
Take a moment to understand what’s happening here before moving on. I encourage you to check the documentation of each of these to understand a bit better how it works, and if you want, try to answer this question:
Which publisher was created by each of the operators? Was it a Just? Future?…
Data Type Transformation
The last operator we used in the previous example was the Map, and the only reason to use it was to show you that we have the power to change the Output and Failure types as we move down the stream.
In this case we started with a sequence of integers, and ended up with a single String value.
Keep this in mind as you create more complex streams, and know that you have the power to transform the data in a simple way.
Type-Erased
There is one more thing that we should know about when working with operators.
As we can see from the example above, there are going to be occasions when working with publishers and operators that will lead us to really complex structure types.
If we were to leave these types like this, you can imagine how difficult it would be to create libraries that would be easy to comprehend, not to mention the level of detail that we would be exposing in our APIs.
For that reason Combine provides us with yet another publisher called AnyPublisher.
What this publisher does is to wrap the previous publisher in the chain and “erases” its signature back to the common type of AnyPublisher. Allowing use to hide the concrete implementation.
This will be, very often, the last operator on your data streams.
If you are curious to explore further what was covered in this article, I’ve created an Xcode Playground that you can use.
It contains a summarized version of the theory with simple examples that you can use to play around with the different components.
Pff that was a lot to cover, but hopefully things are starting to make sense to you, and you are getting a better image of how everything fits and works together.
As last time. Take a moment to digest all of it and I’ll see you in the next part where we will go through a demo project to put the theory into practice.