Transcript
[ Music ]
[ Applause ]
>> 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.
Here's how.
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 ]