Hello. Thank you. My name is Tony Parker, I'm the manager of the Foundation Team at Apple, and today I'm excited to have the privilege to introduce you to our newest framework. It's called Combine.
Let's talk about asynchronous programing.
Here I have an app that I'm working on that allows students to sign up for my brand-new wizarding school. And as you can see, it's got some pretty simple requirements.
First, we need to have a valid username which we're going to check by making a network request to our server. We also need of course to have matching passwords which is something that we can check locally in the app.
And while we do all of these things, of course we need to maintain a responsive user interface, not blocking the main thread. So let's try using it and see how this works.
First, I'm going to start typing the username like, I don't know. How about Merlin. Seems like a pretty good wizard name. And already there's plenty of asynchronous behaviors going on. I used Target/Action to listen for notifications about the user typing. I use a timer to wait for the user to stop typing just for a little bit so I don't overwhelm my server with network requests. And finally, I use something like KVO to listen for progress updates about that asynchronous operation. Now if we continue, we'll find out that we get a response from that request and we have to update our UI. So I pick a new username and then my super-secret secure password of 12345. Please don't use that password, just for demo purposes.
But here now we've done a lot more asynchronous work. We had to wait for the response for our URL session request. We had to merge that result with the result of our synchronous checking and finally I had to do all of those things -- once all of those things were completed, I had to update my UI again using something like KVC.
So across the Cocoa SDK you'll find plenty of asynchronous interfaces. Some you saw here like Target/Action, but there's a lot more, including NotificationCenter and lots of ad-hoc callbacks. These are API's that take a closure or completion block. All of these things have important and different use cases. But sometimes when you need to compose them together, it can be a little bit challenging.
So with Combine we set out not to replace all of these but instead to find what was common amongst them.
And that's what Combine is, a unified declarative API for processing values over time. Combine is written in and for Swift. That means we can take advantage of Swift features like Generics. Generics let us reduce the amount of boilerplate code that you need to write. It also means that we can write generic algorithms about asynchronous behaviors once and have them apply to all kinds of different asynchronous interfaces.
Combine is also type safe, allowing us to catch errors at compile time instead of at runtime. Our main design point about Combine is that it is composition first. What that means is that the core concepts are simple and easy to understand, but when you put them together, you can make something that's more than the sum of its parts.
And finally, Combine is request-driven, allowing you the opportunity to more carefully manage the memory usage and performance of your app.
So let's talk about those key concepts. There's only three: Publishers, Subscribers and Operators. And we'll go over each in turn. First, Publishers.
Publishers are the declarative part of Combine's API.
They describe how values and errors are produced. They're not necessarily the thing that actually produces them.
That means as descriptions, they are value types which in Swift means we use a struct.
Publishers also allow for registration of a Subscriber; that will be something that receives these values over time.
Here's the protocol. It's called Publisher. It has two associated types: the Output, that's the kind of value that it produces, and the Failure which is the kind of errors that it produces. If it's not possible for a Publisher to produce an error, then you can use the type never for that associated type.
Publisher has one key function. It's called Subscribe. As you can tell from the generic constraints on this function, Subscribe requires the Subscriber's Input to match the Publisher's Output, and the Subscriber's Failure to match the Publisher's Failure. Here's an example of a Publisher. This is our new Publisher for NotificationCenter.
As you can see, it's a struct and its Output type is Notifications and its Failure type is Never.
It is initialized with three things, which center, which name and which object. If you're familiar with our existing NotificationCenter API, they should look very familiar. So again, we're not replacing NotificationCenter. We're just adapting it. Next, Subscribers.
Subscribers are the counterpart to Publishers. They are what receive values, including the completion if the Publisher is finite.
Because Subscribers usually act and mutate state upon receipt of values, we use reference types in Swift which means they are classes.
Here's the protocol for Subscriber. As you can see, it has the same two associated types: Input and Failure. Again, if the Subscriber is unable to receive Failures, then you can use the type Never.
And three key functions. It can receive a subscription. A subscription is how a Subscriber controls the flow of data from a Publisher to a Subscriber.
It can also of course receive Input. And finally, if the Publisher that it is connected to is finite, then it can receive a Completion which can be either Finished or Failure. Here is an example Subscriber.
This one is called Assign.
Assign is a class and it's initialized with an instance of a class, an instance of an object and a type safe key path into that object.
What it does is when it receives input, it writes it out to that property on that object. Because in Swift there's no way to handle an error when you're just writing a property value, we set the failure type of Assign to Never. Let's talk about how these fit together.
So you may have some kind of controller object or other type which holds your Subscriber, and that will be responsible for calling Subscribe with the Subscriber to the Publisher, attaching it.
At that point, the Publisher will send a subscription to the Subscriber which the Subscriber will use to make a request from the Publisher for a certain number of values or unlimited. At that point, the Publisher is free to send that number of values or less to the Subscriber. And again, if the Publisher is finite, then it will eventually send a Completion or an Error.
So again, one subscription, zero or more values and a single Completion. So returning to our example, let's say that I have a model object called Wizard and all I care about today is what grade that wizard is in. Let's start with Merlin who is currently in fifth grade. What I want to do is listen for a notification about my students graduating, and once they've graduated, I want to update my model object's value.
So I start with a NotificationCenter Publisher on the default center about graduation of Merlin.
Next, I create an Assign Subscriber and tell it to write the new grade to Merlin's grade property.
Next, I can use Subscribe to attach them. However, as you might expect, this doesn't compile. And the reason is because the types don't match.
NotificationCenter makes notifications but Assign being configured to write to an integer property expects an integer.
So what we need is something in the middle to convert between notifications and integers. What that is, is an Operator.
Operators are Publishers until they adopt the Publisher protocol. And they are also declarative and therefore value types. What they do is describe a behavior for changing values, adding values, removing values or any number of different kinds of behaviors.
And subscribe to another Publisher which we call the upstream, and send the result to a Subscriber, which we call the downstream. Here is an example of an Operator. This one is one that you'll become very familiar with when you use Combine. It's called Map.
Map is a struct that is initialized with which upstream it connects to and how to convert its upstream's output into its own output.
Because Map doesn't generate Failures of its own, it simply mirrors its upstream's Failure type and it will just pass it through.
So with Map we have the tool we need to convert between notifications and integers.
Let's see how.
So keeping the same Publisher and Subscriber and before, I'm adding this converter which as you can see is configured to connect to the graduationPublisher and has a closure. That closure receives a notification and looks for a user info key called NewGrade.
If it's there, and if it's an integer, then we return it from this closure. If it's not there, or if it's not an integer, the new use a default value of zero. What that means is that no matter what, the result of this closure is an integer and therefore I can connect this to the Subscriber. And everything connects, compiles and works.
Now all of this construction syntax can get a bit verbose, so we also have a more fluent syntax. Here's how it works. As an extension on the Publisher protocol, meaning it's available to all Publishers, we added a series of functions named after each Operator. Here is the one for Map.
As you can see, its arguments are everything needed to initialize a Map except for upstream. And the reason is because as an extension on Publisher we can simply use self.
Now this may seem like a pretty trivial convenience, but actually this is what's really going to transform how you think about asynchronous programing in your app.
Let's return to our example but use the new syntax. So here I am starting with the NotificationCenter Publisher for graduated on Merlin.
Once I receive a notification, I then map it using the same closure as we saw earlier, and then I assign it to the grade property on Merlin.
And you can see this syntax provides a very linear, easy-to-understand flow of what happens step-by-step.
Assign returns something called a cancelable. Cancelation is also built into combine. Cancelation allows you to tear down the sequence of Publishers and Subscribers early if you need to.
So this step-by-step syntax is really the heart of how you use Combine. Each step describes the next set of instructions in a chain. Transforming values as they make their way from the first Publisher through a series of Operators and ending in a Subscriber. And we have a lot of these Operators. We call them our Declarative Operator API.
They include functional transformations like Map. We also have Filter and Reduce, list operations like taking the first, second or fifth element of the Publisher.
Error handling like turning an error into a default or placement value. Thread or Q Movement, for example moving heavy processing work to a background thread or UI work to the main thread. And scheduling and time, including integration with from loop, dispatch queue, support for timer, timeouts and more.
And with so many of these operators available, it can be perhaps a bit overwhelming to think about how you will navigate amongst these. So what I encourage you to do is return to our core design principle about Combine, and that is composition.
Instead of providing a few operators that do a lot, we provide a lot of operators that just do a little bit each, making them easier to understand.
So to help you navigate amongst all these operators, we drew inspiration for their names from the existing Swift Collection APIs.
Let's imagine a quadrant graph. So on one side I have synchronous APIs and the other asynchronous. On the top I have single values and on the bottom I have many values.
So in Swift, if you need to represent an integer synchronously, you might use something like int. If you need to represent many integers synchronously, you would use something like an array of integers.
In Combine we took these concepts and mapped them into the asynchronous world. So if you need to represent a single value asynchronously, it comes later, we have a future. If you need to represent many values asynchronously, that's a Publisher.
So what that means is that if you're looking for a particular kind of operation that you already know how to do with an array, try using that name on a Publisher.
Let me show you an example.
So here I chose to use a default value of zero if the key was not present or if it was not an integer. Maybe instead it would be a better idea to not allow this bad value to progress and end up written into my model object.
So one thing I could do is allow this closure to return nil and then filter out the nil values.
Well, in Swift 4.1, the standard library introduced a name for that operation. It's called compactMap. And so Publisher has one too. And it behaves in a very similar way. If you return nil from this closure, then compactMap will filter it out, keeping it from progressing further down the stream.
Let's build up our step-by-step instructions using a few more familiar names.
Let's say that only students in fifth grade or higher are allowed in my school. I can do that using Filter. Filter takes a predicate and only allows elements that pass that predicate to proceed. This is exactly the same behavior as Filter on Array.
Let's say furthermore that you're only allowed to graduate a maximum of three times.
So on Array, if you need to take the first three elements, you could use prefix 3. On a Publisher, if you want to receive the first three elements only, you can use prefix of 3. What it does is after it receives three values, it will Cancel the upstream and send a Completion to the downstream. So stepping back, let's see what we have here. We have a NotificationCenter Publisher that listens for graduations on Merlin.
Once he graduates, we will fetch the NewGrade out of that property, out of that Notification. And then we will make sure that the value is greater than fifth grade and that it has only happened a maximum of three times before finally assigning it to the grade property on Merlin.
Now Map and Filter are great APIs but they're primarily for synchronous behaviors. Combine really starts to shine when you're working in asynchronous world.
So here are two more operators that I'm going to talk about that can be really useful for that. First, Zip.
So let's say in my app before the user is allowed to continue, they need to wait for their wand to be created which is three long-running asynchronous operations like this.
So the Continue button becomes enabled once all three things are finished. This is a job for Zip.
Zip converts several upstream inputs into a single tuple.
Because it requires input from all of its upstreams in order to proceed, it makes it a kind of when/and operation as in, when this and this and this have finished, do this other thing.
So for example, my first Publisher produces A, and then when my second Publisher produces a 1, I now have enough information to create a tuple and send that value downstream to my Subscriber.
In my app, I use the version of Zip that takes three upstreams to await the result of three asynchronous operations that each give me a Boolean result. So I map the tuple into a single Boolean and here I've written it into the isEnabled property on the button to turn it on.
So after you're done waiting for your wand to be created, like everybody else, my students have to agree to a set of terms and conditions before they are allowed to proceed to playing with their wands. What that means is that all three of these switches have to be enabled before the Play button is enabled. However, if one of them is then later disabled, we need to disable the button. This is a job for Combine Latest.
Like Zip, it converts several upstream inputs into a single value. However, unlike Zip, it requires an input from any of its upstreams to proceed, making it a kind of when/or operation.
In order to support that, it stores the last value that it's received from each upstream. And it's also configured with a closure that lets you convert that into a single downstream value.
So for example, when my first Publisher produces A, and my second Publisher produces A1, I then run my closure which stringifies this and sends it downstream. Later, when the second Publisher produces a new value, I can combine it with the value from previously from the first Publisher and send that new value down.
That means that I get new events as any upstream changes.
So in my example app, I used a version of CombineLatest which takes three upstreams, the Boolean states of all three of those switches as they change, convert them into a single Boolean value again and write that to the isEnabled property on my Play button.
That means that if any of them are false, the result is false. But if all of them are true, then the result is true, thus enabling the button. So we designed Combine to be adoptable incrementally in your app. You don't have to convert everything over to use this. So to get started, I have a few suggestions on places that you might find in your app today that you can use Combine for. For example, if you use NotificationCenter, you receive notifications and then you look inside them to decide whether to act or not, try using Filter. If you weight the result of several asynchronous operations, then you can use Zip, including network operations.
And finally, if you use URL Session to receive some data and then you convert that data into your own objects using JSON Decoder, we have an operator that will help with that as well. It's called Decode.
So we went over the basics today. Publishers, Subscribers and Operators. However, there's a lot more to Combine. And that includes error handling and cancelation, schedulers and time, and some great design patterns including using Combine in different modules or between different areas of your app. And of course integration with SwiftUI. For more on that, please watch Combine In Practice.
That's all I have today. Thank you so much for your time. [ Applause ]
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.