스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Combine in Practice
Expand your knowledge of Combine, Apple's new unified, declarative framework for processing values over time. Learn about how to correctly handle errors, schedule work and integrate Combine into your app today.
리소스
관련 비디오
WWDC19
-
다운로드
Hello. 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 -- 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. 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 ]
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.