Transcript
[ Music ]
>> Hello.
[ Applause ]
Hello. My name is Michael LeHew
and I work on the Foundation
Team at Apple.
And today I'm really excited to
talk to you about the new
Combine Framework that we're
releasing this year.
And just to clear things up,
we're not talking about
tractors.
Before I go in-depth, I want to
start off with a brief overview
of what Combine is all about.
Often in our code, we have many
places where we have some sort
of value or event Publisher and
some Subscriber interested in
receiving values from that
Publisher.
And some interested party comes
along and establishes a
connection between these two
parties.
Once established, the Subscriber
sometimes declares that they are
interested in receiving values
from that Publisher, after which
the Publisher is free to begin
sending values downstream.
And this goes on until either
the Publisher decides to stop
sending values, whether because
it finished or there was some
sort of failure, or by someone
choosing to cancel the
subscription.
And as you've seen, this general
shape of communication appears
throughout our software, whether
it's callbacks or closures or
any other situations where
there's asynchronous
communication.
And it's this pattern that
Combine is all about.
With Combine, we define a
unified abstraction that
describes API that can process
values over time.
Let's take a look at the
specifics of what it means to be
a value Publisher.
Now we've already talked a lot
about this in our introduction
session, but to review, value
Publishers in Combine conform to
the Publisher protocol.
They specify two associated
type: their output which is the
kinds of values that they
publish and whether or not they
can fail.
And I'll have a lot more to say
about failure in a bit.
Publishers also describe how to
attach Subscribers to themselves
with the constraint that the
associated types must match.
And that's it.
All right.
I think that's enough theory for
right now.
This session is called Combine
in Practice, so let's actually
practice.
So I have a wizard friend.
He's really, really cool and he
wants to work on an app together
for a new wizard school that
he's founding.
One of the features that we want
to have in this app is going to
let you download super neat
magic tricks that have been
shared by wizards just like him.
Now he's not an app developer.
He's a wizard, so he gave me a
sketch, so this is my UI comp
that I get to work with.
Now he is a wizard but he does
know how to write code, enough
code to go and download a magic
trick for me.
And so he's going to go off and
do that.
And what I'm going to do is I'm
going to talk about how we are
using Combine to get to the
application values that we need
to say populate this label with
the name of a magic trick.
With Combine, NotificationCenter
will support exposing its
notifications with Publishers.
And so we'll go ahead and create
a Publisher for the notification
that my wizard friend is going
to deliver.
Now the return type of this
function is going to be a
Publisher, but in Combine what
really matters for a Publisher
are what its output and failure
types are.
NotificationCenter Publishers
deliver notifications and can
never fail.
And since we're going to be
talking a lot about Publishers,
I'm going to use this convention
of showing the output of a
Publisher on top and the failure
on the bottom for the rest of
our discussion.
So we have a notification
Publisher, but what we really
want is the data inside that
describes the magic trick that
we've just downloaded.
My friend told me he put the
data in the user info dictionary
and lucky for us, Combine offers
a really useful map function
that lets us reach inside and
transform the notification to a
form we need.
This is very similar to
operations that already exist on
Sequence.
And now we can see that we're
working with a Publisher whose
output is data that can never
produce an error.
We call functions like map that
act on Publishers and return new
Publishers' operators.
And they come up a lot in
Combine.
Now my friend also told me that
the JSON payload -- or the data
will be a JSON payload of a type
that we've already defined in
our application.
So I can use another Combine
operator to attempt to decode
the data, and we call this
operator tryMap.
It's just like map except it
adds the ability to transform
any errors thrown into a failure
in the stream.
And indeed, the output of this
operator will be a Publisher of
magic tricks where the failure
conforms to the Swift error
protocol.
Decoding custom types from data
is such a common task that we
actually provide an operator
that takes care of this for you.
Simply call decode.
The Publisher's output --
[ Applause ]
The Publisher's output and
failure types will remain
exactly the same.
Now that we have a Publisher
that can fail though, I'd like
to talk a little bit more about
the things we can do.
In Combine, properly reacting to
potential failures is incredibly
important.
Every Publisher and Subscriber
gets a chance to describe the
exact kinds of failures that
they produce or allow.
And we built this into Combine
because just like Swift, we
didn't want to leave error
handling to be something that
was purely convention-based.
We tried that in other
languages.
It didn't work out so well.
And so many types describe their
failure types as never.
And this is to indicate that
they can fail or that they
expect failure to be handled
earlier in the stream.
But for everything else, we
offer many operators that allow
you to react to and recover from
failure should it arise.
One of the simplest is just to
assert that failure can never
happen.
Not surprisingly, the failure
type of the return Publisher
will now be never.
But let's look at why.
Imagine a situation where we
have an upstream Publisher
connected to a downstream
Subscriber with an
assertNoFailure operator in the
middle.
Now this operator will happily
just forward values along should
they be received.
However, if an error arrives
from upstream, our program will
simply trap, and that's really
not the most magical outcome for
our wizardly customers.
Lucky for us, we have a lot of
other operators for working with
failure and combine.
In addition to asserting, we
allow you to attempt to retry
the connection to the upstream
Publisher or to transform the
error to another type.
A particularly useful operator
is catch.
catch lets you provide a closure
that defines a recovery
Publisher that will be used in
the case if failure arose on the
original upstream Publisher.
I'd like to take a look at how
this works.
We'll start with a similar
picture as before, except
instead of assertNoFailure we'll
use the catch operator.
As before, values will happily
forward along down to the
downstream Subscriber.
However, when an error arrives,
the existing upstream connection
will be terminated.
We'll then call the provided
Recovery closure which will
produce a new Publisher which we
then subscribe to and are free
to receive values from
henceforth.
In this way, the catch operator
lets us recover from an error by
replacing the original Publisher
with a new one.
Let's go ahead and use this in
our code now.
Using catch is pretty much the
same as any other operator,
although the closure here
expects for us to return a
Publisher.
Combine defines a special
Publisher for when you already
have a value that you want to
publish.
We call it Just, as in just
publish this value.
And it's one of the many
examples of Publishers that
Combine comes with from the
start.
And with that, the type of our
return Publisher can no longer
fail.
Now at this point I'd like to
review the different
transformations we've already
done.
We started with our Publisher of
notifications, which we then
mapped over to get to the data
that we knew that we wanted to
decode.
Afterwards, we made use of the
decode operator to transform our
data into a user-defined type.
But because decoding can fail
for myriad reasons, we account
for that by replacing the
upstream with a placeholder
should failure arise.
But wait, once we switch to the
Recovery Publisher, we're never
going to see another
notification again.
We terminated that subscription.
What we really want is the
ability to try to decode and if
that fails, use a placeholder
while maintaining a connection
to the original upstream.
Not surprisingly, Combine has an
operator for that.
We call it flatMap.
flatMap works a lot like map,
hence the name.
You're given values from the
upstream Publisher with the
expectation though that you're
going to produce a new Publisher
from that value.
flatMap will then handle the
details of subscribing to this
nested Publisher offering its
values downstream.
So let's take a look at how this
works before we jump back to the
code.
As before, values are going to
arrive from upstream into our
flatMap operator.
Once there, flatMap will call a
closure to transform that value
into a new Publisher, and in
this case this new Publisher is
a Just followed by a decode and
a catch.
Similar to before.
flatMap will then subscribe to
this new Publisher, offering the
resulting values downstream.
I'd like to trace through
another value in this flatMap.
But this time let's imagine that
the decode threw an error during
the operation.
When the failure reaches the
catch, it will then be replaced
with the recovery Publisher.
And this will be the Publisher
that is returned to the flatMap.
Thus guaranteeing that that
operation can never fail.
I'd like to take a look now at
using this in code.
We'll start where we left off,
where we were handling the first
error of our stream.
But now we'll introduce the
flatMap operator.
And it's really a rather simple
transformation.
As with catch, we'll use just a
form, a new Publisher from the
data that we received.
This is the data that we just
decoded from the map operator.
Using the nested scope for the
flatMap operator, we will
return, we will decode, we will
catch, return that to the
flatMap.
Which flatMap will then
subscribe to this Publisher, and
the resulting Publisher will be
a Publisher of magic tricks that
can never fail.
Now that we've handled our
upstream failures, let's go
ahead and do what we originally
wanted to do, and that is to try
to publish this particular magic
trick's name.
With Combine, this is as simple
as using another operator, the
Publisher(for:) operator.
And we use this to reach inside
the ProduceMagicTrick via a
type-safe key path, producing a
new Publisher, in this case a
Publisher of strings.
At this point, I want to talk
about a final kind of operator
that provides some pretty
powerful functionality.
We call them scheduled operators
and just like scheduling things
in real life, scheduled
operators help you describe when
and where a particular event is
delivered.
The operators are natively
supported by the RunLoop and
DispatchQueues and some examples
of these operators include
operators like delay which defer
the delivery of an event until
some Future time.
There's also throttle that
guarantees that events are
delivered no faster than a
specified rate.
We also have operators like
receive(on:) which guarantee
that downstream received events
will be delivered on a
particular thread or queue.
We'll use that operator now and
guarantee that our magic trick's
name will always be delivered on
the main queue.
And we see here that the output
and failure types are unchanged.
And this ends up being pretty
common with scheduled operators.
And so let's review the rest of
our Publisher chain.
We left off with flatMap.
And then we used Publisher(for:)
to reach inside our magic trick
and extract the magic trick's
name.
Finally, we move our work to the
main thread with the
receive(on:) operator.
And now if we're working with
AppKit or UIKit where the UI
needs to be updated on the main
thread context, we're ready to
go.
The published values are already
on the right thread.
And as you've seen, we've been
able to do quite a lot with
Publishers and their operators
so far.
We started with an initial
recipe with each operator along
the way offering a new tweak for
producing strongly typed values
over time.
We've seen that Publishers can
produce their values
synchronously as was the case of
Just.
And asynchronously such as
NotificationCenter.
At this point though, I want to
focus on the other side of
publishing values.
And that is receiving them.
I'd like to talk about
Subscribers now.
Just like Publishers,
Subscribers in Combine have two
associated types: their input
and the kinds of failure that
they allow.
They also describe three event
functions for receiving a
subscription, values and a
completion.
The order that these functions
will be called is well-defined
and comes down to following
three rules.
Rule number one, in response to
a subscribe call, a Publisher
will call receive(subscription:)
exactly once.
Rule number two, a Publisher can
then provide zero or more values
downstream to the Subscriber
after the Subscriber requests
them.
Rule number three, a Publisher
can send at most a single
completion and that completion
can indicate that the Publisher
has finished or that a failure
has arisen.
And once that completion has
been signaled, no further values
may be emitted.
These three rules can be
summarized as follows.
A Subscriber will receive a
single subscription followed by
zero or more values, possibly
terminated by a single
completion indicating that the
publish finished or failed.
And I say possibly there because
the completion is optional.
Many given streams can be
potentially infinite, like the
NotificationCenter example from
before.
In Combine, we support many
different kinds of Subscribers.
And I'd like to show you how
they work.
Let's go back to our Publisher
example, except what we really
just need to know right now is
the kind of Publisher that we're
working with.
So let's go ahead and make some
room.
And add a Subscriber.
Here I've added one of the
simplest forms of subscription
in Combine, key path assignment,
using the assign(to: on:)
operator.
And this will ensure that any
values emitted by the upstream
Publisher will be assigned to
the specified key path on the
specified object.
And from this point, right,
we're free to basically take any
Publisher and assign to any
property from the value which is
pretty powerful.
This operator also produces a
cancellation token that you can
later call to terminate the
subscription.
I'd like to talk a little more
about cancellation.
We built cancellation into the
shape of Combine because it's
often advantageous to be able to
terminate a subscription before
a Publisher is done delivering
events.
This is especially true when you
want to free up resources
associated with that
subscription.
Cancellation of course is best
effort, but it offers a means
for you to unsubscribe a
Subscriber should you need to.
We introduce a new protocol for
describing things that can
cancel or be canceled.
And we introduce a really,
really super helpful convenience
called AnyCancellable which
carries the added benefit that
it will automatically call
cancel on deinit.
This dramatically decreases the
number of times that you're
going to need to call cancel
explicitly.
Just rely on the powerful memory
management capabilities already
provided by Swift.
So let's go ahead and look at a
second form of subscription.
This is using a sink operator.
And these are really fantastic.
You just provide a closure and
now every value received, your
closure's going to get called
and you can do whatever side
effecty thing you want to do.
As with assign, sink will return
a cancellable that you can then
use to terminate the
subscription.
A third form of subscription is
a little bit of a hybrid.
We call them subjects and they
behave a little bit like a
Publisher and a little bit like
a Subscriber.
They typically support
multicasting their received
values, and of particular
importance they let you send
values imperatively.
And this is of paramount
importance when you're working
with existing code bases.
Let's take a look at how they
work before showing how we use
them in practice.
As I mentioned, with a subject,
it's often possible to broadcast
to multiple downstream
Subscribers, as well as
imperatively send a value.
And any value received will be
broadcast to all downstream
Subscribers.
This is also true if the value
is produced by an upstream
Publisher.
In Combine we support two kinds
of subjects, a Passthrough
subject which stores no value,
and so you'll only see values
after you subscribe to the
subject.
We also support a CurrentValue
subject.
This maintains a history of the
last value that it received,
allowing new Subscribers an
opportunity to catch up.
Now we'll look at these in
action.
We'll start as before with our
Publisher.
Creating the subject is as easy
as picking which one you want,
specifying the output and
failure types and calling a
constructor.
Subjects behave like Subscribers
in that they can subscribe to an
upstream Publisher.
As well as like a Publisher by
calling any of the operators
that I've talked about today,
including things like sink, to
form Subscribers to themselves.
You can even send values
imperatively, such as this very
magic word.
And in fact, subjects arrive so
often that we even define
operators for injecting subjects
into your streams, like Share
which injects a Passthrough
subject into a stream.
Subjects are very, very
powerful.
You're going to really find lots
of cool uses for them.
And with that I'd like to
actually switch and talk to a
fourth and final kind of
Subscriber, and that is
integrating with SwiftUI.
One of the amazing things about
SwiftUI is how you only need to
describe the dependencies in
your application and the
framework takes care of the
rest.
From the perspective of Combine,
this just means that you need to
provide a Publisher that
describes when and how your data
has changed.
To do so, you just simply
conform your custom types to the
BindableObject protocol.
BindableObjects in SwiftUI have
a single associated type.
It's a Publisher that is
constrained to never fail.
And this is fantastic for
working with UI frameworks
because the type system of the
language is going to enforce
that you handle upstream errors
before you get to your
Publisher.
Finally, you specify one
property and this property
called didChange yields the
actual Publisher that notifies
when your type has changed, and
that's really it.
For more on how data flow works
in SwiftUI, I strongly encourage
that you check out the Data Flow
in SwiftUI talk where we go into
considerably more detail about
all the great things that are
possible here.
But for a taste, I'd like to
show you how this can work in
practice.
We'll start with an existing
model from within our wizard
school application.
We'll then add conformance to
BindableObject.
And here we're going to use a
subject to describe when our
model object has changed.
And we really don't need our
subject to signal any specific
kinds of values because the
framework will figure that out
by what we call from our body
method.
And so we'll choose void as the
output type of our subject.
Using a subject like this offers
a lot of flexibility, since now
we can imperatively send
messages any time our object has
changed.
For now, though, let's go ahead
and just use a couple of
property observers and directly
call send on the subject to
indicate that our model object
has changed when either of our
properties has changed.
Next, we need to hook this model
up to a SwiftUI view which we do
with the following.
We'll declare a model as being
an object binding which allows
SwiftUI to automatically
discover and subscribe to our
Publisher.
And then we'll refer to the
model's property from within the
body property.
And that's really it.
SwiftUI will automatically
generate a new body whenever you
signal that your model has
changed.
Now I've shown you that Combine
has a ton of built-in
functionality that you can
compose to create some pretty
powerful things.
We are really excited for the
kinds of simplifications to
asynchronous data flows that are
going to be possible with this
new framework.
And to help show this, my
colleague Ben is going to come
and talk to you about how to
integrate all this great
functionality even further into
your existing applications.
Thank you.
[ Applause ]
>> Thanks, Michael.
I'm excited to be here with
y'all today.
We designed Combine with
composition in mind.
As you saw with Michael's
example, we took an initial
small Publisher and through many
different transformations
created the eventual Publisher
that we wanted.
So let's see an example.
We have to sign up for our
application that we'd like, to
allow our wizards to sign up for
our wizard school.
And we have a few requirements.
First, we need to make sure that
the username is valid according
to our server.
Second, we have a password field
and a password confirmation.
We'd like to ensure both match
and are greater than eight
characters.
And finally we want to make sure
that if all of these conditions
are met, we can enable or
disable our UI.
So in this one example, we had
asynchronous behaviors, we have
some synchronous behaviors that
are local to device, and then we
need to be able to combine them
all together.
So let's see how Combine can
help us with that.
To start off, I'll use Interface
Builder to create a target
action on the value change
property for our password
fields.
And then using that in code,
we'll get a signal any time the
user's typing into those fields.
We'll take the text property of
those current values and we'll
store it into an ivar.
But we wanted to compose these
with other behaviors, the
synchronous behaviors that we
talked about earlier.
So how can we do that?
It's really easy.
By adding Published to our
individual properties, we can
add a Publisher to any one of
them.
Published is a property wrapper
which uses a new Swift 5.1
feature and adds a Publisher to
any given property.
So let's see how we can use this
with some simple examples.
The Published property wrapper
is added before the given
property you'd like to add one
to.
And when we use it in code, it's
just like it was before.
We can also store it and we'll
get a string value.
So in this case, currentPassword
is now the string 1234.
Where it becomes special is when
we refer to it with a dollar
prefix.
In that case, we're accessing
the wrapped value.
We than can use all the
operators that we normally would
on a Publisher or subscribe to
it, in this case using sink.
And then if we were to set that
property again to another great
password "password", our
Subscribers will get that value
when it's changed.
Obviously, this person has not
paid attention to password
hygiene.
We talked about needing to have
our two Publishers evaluated at
the same time.
We added our Published property
to these and added two
Publishers, the published
strings and can never fail.
What we want to end up with is
something that publishes a
single validated password.
Well, we have an operator for
that and it's called
CombineLatest.
So here are our two properties
as we talked about before.
And using CombineLatest we can
refer to the property wrappers
with the dollar sign prefix and
then we'll get this signal when
either one of these changes.
So for example, if the user has
already typed in the password
field and then now is starting
to type in the password
confirmation field,
PasswordAgain will be changing
while Password will be the
original value that they typed
in the first field.
We then can use the closure to
ensure that we meet our business
requirements, in this case if
they both match and if they're
greater than eight characters.
We'll return nil if it's not
because we're going to use this
signal along with the other
signals to determine whether or
not our form is valid.
And to do that, we'll use nil as
our signal.
So as you see in the types, the
types reflect all the steps we
took along the way.
You basically can read it
exactly like the code.
We took two published strings,
we combined their latest values
and we ended up with an optional
string.
But what if we got a requirement
that we wanted to make sure that
people don't use these bad
passwords and we add a map?
You'll see that the type changes
here.
It now says that we combined the
latest values of two published
strings and then mapped it to
result in an optional string.
That's awesome and that's great
for debugging in almost every
other use case.
But in this case we're
advertising this as an API
boundary and we want to compose
it with other Publishers.
So wouldn't it be great if we
could just focus on what's
important here?
Which is that it is a Publisher
of optional strings that can
never fail.
To do that, we have an operator
called eraseToAnyPublisher which
then returns an AnyPublisher of
optional string never.
You'll see that the types
haven't changed but it does mean
that we can advertise the exact
contract we want for our API
boundary and hide all the
implementation details along the
way.
So taking a look at what we've
done so far, we took our initial
properties that were strings, we
added a string Publisher to it
using the Published property
wrapper.
We then used CombineLatest to
combine the latest values of
these two Publishers, and add
our business logic.
We then used map to filter out
those bad passwords and finally
we used eraseToAnyPublishser
because this is an API boundary
and we're going to compose this
with other things.
So awesome.
We have our first Publisher.
Moving on to the next, we have
some asynchronous activities
we'd like to model here.
We want to make sure that the
username is validated according
to our server which is going to
have a user typing in rapidly.
So like before we add Published
to our string property storage,
and we're going to hook up a
target action for the
valueChanged property.
But in this case it's a little
special, because we don't want
to have a network operation
happen every single time the
user types a single character.
Otherwise we'd spam our server.
What we want to do is smooth the
signal out just a little bit.
And for that we have debounce.
Debounce allows you to specify a
window by which you'd like to
receive values and not receive
them faster than that.
Let's see how that works as an
example.
So here we have our upstream
Publisher.
In this case that would be a
text field and we have our
debounce in the middle.
If the user types quite quickly,
you'll see the rapid signals.
But then we can smooth that out
to have a single signal within
that window.
That's great, but we can do a
little bit better.
If the user is typing within
that window and the values at
the end are always going to be
the same, there's no reason to
hit the server again to see
whether that same username is
valid.
So if the user types Merlin, we
get that value, deletes the n
and types the n again, Merlin
again, we don't need to hit the
server again.
removeDuplicates is our operator
for that.
It will make sure that we don't
get the same values published
over and over again within that
window.
So using in code, we have our
username property that we added
Published to.
We then use the debounce to
smooth out our signal a bit.
And finally remove the
duplicates.
But we haven't handled any of
the asynchronous stuff yet.
We just smoothed our signal out.
What we want to do is actually
hit our server and find out
whether or not this is valid.
For that we have an existing
function in our application
called usernameAvailable.
And what I'd like to do is bring
this in as a Publisher.
So from Michael's example we
learned that flatMap allows you
to take a value from your stream
and then return a new Publisher.
So how can we call this?
Well, for that we have something
called a Future, and when you
construct one you give it a
closure that takes a promise.
A promise is just another
closure that takes the result,
either as success or a failure.
And when we use it, it's pretty
straightforward.
We call our usernameAvailable
function and when it
asynchronously completes and we
have the value, we fill our
promise with the success in this
case.
And like before, we'll state
that if it's not available, it's
a nil.
So reviewing those steps, we had
our simple Publishers at the
beginning, our username
Publisher.
We debounced it to smooth the
signals out and we removed any
of the duplicates within that
window.
We then used Future to wrap our
existing API that makes an
asynchronous network call.
And we used flatMap to fork our
stream in that way.
Then we erased it to any
Publisher because this is an API
boundary.
And so now we have these two
custom Publishers that we've
made, both validatedPassword and
validatedUsername.
And we're ready to combine them.
So now what we want to do is
take these two signals, one
that's local to device and the
other being an asynchronous
network call, and use those to
enable or disable our UI.
Well, we already know how to do
this.
We use the CombineLatest
operator.
We'll take those two Publishers
that we made before.
We'll check that they're valid
and in this case we'll just
return a tuple with our full
credentials as an optional or
nil if they're not.
And actually wiring this up to
your UI is pretty simple.
We wire up an outlet to our Sign
Up button.
We'll create an ivar to store
this subscription so that we
keep it for the entire lifetime
of this ViewController.
Because we want to enable and
disable the button or the entire
time the form is shown.
So we'll store it.
We'll map this to a Boolean
because we want to assign this
to the isEnabled property on the
button.
Finally, we'll use receive(on:)
to switch over to the main
RunLoop which is what we need to
do for any UI code.
And then we'll use the assign
operator to assign it to the
given key path (on:
signupButton).
So awesome.
We have all the parts we need.
So stepping back, we started
with these three very simple
Publishers that just publish
strings.
And then using composition we
built this up from small little
steps as we went along to create
our final chain, and then
compose those and assign them to
the button.
This is really what Combine's
all about.
So I suggest you get started
right away.
Compose small parts of your
application into custom
Publishers, identify small
pieces of logic you can break up
into little tiny Publishers and
use composition along the way to
chain them all together.
You can totally adopt
incrementally.
You don't have to change
everything right away or, you
know, you can mix and match.
We saw with Future how you can
bring things in that you already
have today.
You can compose callbacks and
other things using Future like
we saw.
For more information, check out
our Introducing Combine talk and
the Data Flow Through SwiftUI
talk as well.
And we'll be at the AppKit labs
later today as well.
Thank you.
[ Applause ]