스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Data Flow Through SwiftUI
SwiftUI was built from the ground up to let you write beautiful and correct user interfaces free of inconsistencies. Learn how to connect your data as dependencies while keeping the UI fully predictable and error free. Familiarize yourself with SwiftUI's powerful data flow tools and understand what the best tool is for each situation.
리소스
관련 비디오
WWDC19
-
다운로드
Good morning and welcome to Data Flow Through SwiftUI. My name is Luca Bernardi and I will later be joined by my friend and colleague, Raj Ramamaurthy. Are you guys excited about SwiftUI? Yeah. Sweet. I'm super excited to be here today. SwiftUI is the shortest path to a great app, but we also designed it from the ground up with the goal of solving the complexity of UI development. That means that data is a first class citizen in SwiftUI. In this talk, we are going to show you the simple but powerful tool that you have at your disposal to flow data through your view hierarchy. This tool can help you achieve app that are not only beautiful but also well behaved.
We will also look under the hood of how SwiftUI updates your view hierarchy to guarantee that you always have a correct and consistent representation of your data.
And finally, we will empower you with a mental framework for understanding your data and the tool available to you.
But before moving on, what do we mean by data? Data is all the information that drives your UI. Data comes in all sort of shapes and form. One example is stating your UI like the state of a toggle.
Data also represent model data, such as the object that drives a list of messages.
We have a number of tool at your disposal depending on what you want to achieve. You've probably seen some of this tool in previous talk. But if you are not familiar with them, don't worry we're going to explain what they are and when to use them.
At the end of this talk, you'll know exactly what tool to use and when. But before showing you this tool, I would like to illustrate the two guiding principle that inspired us in designing them. The first principle is that every time you read a piece of data in your view, you're creating a dependency for that view. This is a dependency because every time the data changes, your view has to change to reflect the new value.
For example, here in blue I have a view showing some playback control and the view needs to read some data in purple. Every time this value changes, we need to update the view.
And so additionally, defining this dependency is a manual process and it can quickly become a complex endeavor. But just like viewing SwiftUI are declarative, data dependency are as well. There is no manual synchronization or invalidation. With SwiftUI, you simply describe the dependency to the framework using few tools and the frameworks handles all the rest. And that means that you can focus on building the best experience for your user.
The second principle is that every piece of data that you're reading in your view hierarchy has a source of truth.
The source of truth can live in your view hierarchy. For example when you have some state about something should be collapsed or not, or it can be external such as when you're displaying a message coming from a persistent model.
Regardless of where the source of truth live, you should always have a single source of truth.
Duplicated source of truth can lead to bug and inconsistency because you always have to be careful in keeping them in sync.
Think about all the time you've duplicated the same piece of data, for example, into sibling view and how all the complexity of posting notification, KV observing or responding to a different sequence of event has caused your bug.
And it's pretty easy to get wrong. And they've made the same mistake many time. What you shall do instead is lift up the data into a common ancestor and let the two children have a reference to it.
When you have a single source of truth, you're eliminating inconsistency bug between view and your data. And you can use the tool available in the language to enforcing variant in your data. With this principle in mind, take a step back, look at all the source of truth in your code, and then use this information to make decision about the structure of your data. So I really like listening to a good podcast during my commute to Apple Park. And I thought it will be great to build a podcast player with SwiftUI. We are going to use this example throughout the talk to demonstrate all the tool available to you.
Here is the UI that we are going to build.
It's the interface for the player when we're going to display the show and episode title, the playback button, and the current time. We are going to build this UI step by step. But let's start by creating a view that show the current episode.
We can start by creating a new view, Playerview, and this view has one property. That stored the current playing episode. And we want to display the episode show and title. So in the body, we'll make a VStack containing two texts so that they're stacked vertically. And regular Swift property are your first tool. They're great when you have a view that needs to read all the access to a derived piece of data.
This is data that will be provided to the view by its parent.
But now, I want to be able to play and pause, so let's have that. We had a new property indicating whether the episode is playing or not. And now the image that show a different clip depending on the value of isPlaying. But now I want to make the Play button interactive so that when the user taps on it, would toggle the Playback state and change the image as well. And we can do that by using a button. A button takes some content and an action to execute when the user taps on it.
In the action, we just toggle isPlaying. But if we go to build a run, we get a compiler error.
But this is good because it's keeping us on the right path and through to one of the general principles of UI. We don't mutate the view hierarchy. Every time your UI is updated, it's because some view body is generating a different value. And in order to handle case like this, we have a tool that's called State.
So let's have home state through this view. We can do that by using the state property wrapper on the isPlaying property. And by doing this, we are telling the system that isPlaying is a value that can change over time and that the PlayerView depends on it.
Now if we build a run, we won't get a compiler error. And when the user taps on the button, the state value changes and the framework will generate a new body for this view. If you're not familiar with Property Wrapper, it's a powerful new feature in Swift 5.1. But we're not going into much detail about how they work. And if you want to learn more, check out these two great session. For this talk, all you need to know is that when you add the Property Wrapper, you're wrapping this property and augment it with some additional behavior when it's read or written. And you might wonder, how does this work? What is this additional behavior with state? When you state the framework, allocate the persistent storage for the variable on the view we have and track it as a dependency because if the system is creating storage for you, you always have to specify an initial constant value. View can be recreated often by the system, but with state, the framework knows that it needs to persist the storage across multiple update of the same view. And it's a good practice to explicitly mark state property as private to really enforce the idea that state is owned and managed by that view specifically. But I want to take you behind the scene and show you what's happening when the user is tapping on the button.
Let's start with view hierarchy we just showed.
We just say that when-- that when we define some state, the framework is allocating persistent storage for you.
One was the special property of state variable is that SwiftUI kind of start when they change. And because SwiftUI knows that the state variable was writing the body, it knows that the view rendering depend on that state variable.
When the user interact with the button, the framework execute its action which in turn will meet at some state.
The runtime the data-- the state does change and starting validating their view that owns the state, which mean it will recompute the body of that view and all of its children. In this sense, all the changes always flow down through your view hierarchy. And we're able to do these very efficiently because the framework is comparing the view and rendering again only what is changed. This is exactly what we mentioned earlier when we say that the framework manages the dependency for you.
But we also talk about source of truth before and you should remember that every time you declare a state, you define a new source of truth that is owned by your view. And while this is so important that I'm showing to you in big letter, another important takeaway is that view are a function of state, not a sequence of event. Traditionally, you respond to some event by directly mutating your view hierarchy. For example, by adding or removing a subview or changing the alpha.
Instead, in SwiftUI, you mutate some state and the state work as a source of truth from which you derive your view. This is where SwiftUI, declarative syntax shine. You describe your view given the current state. And this is also how SwiftUI helps you manage the complexity of UI development allowing you to write beautiful and correct interfaces.
You can think of your app as a constant feedback loop between your user and the device.
It all start with your user.
The user interact with your app generating an action. The action is executed by the framework and mutate some state.
The system detect that the state has changed and so it knows that it needs to update the view that depends on the state.
This update will produce a new version of your UI that your user can interact with.
This model where data always flow in a single direction, instead is your single final point for all the changes, make the view update predictable and simple to understand. And now that we understand state, I want to go back to the AppWare building and make some improvement. So the first thing I want to change is that whenever the user taps on Pause, the episode title should change to gray. And we already know how to do that. We can just use our isPlaying state and choose the right text color. Next, I love some good refactoring. And if you have seen the SwiftUI essential tool, you already know that view are a locust obstruction in SwiftUI.
You shall not be afraid of grouping meaningful piece of data in your view into smaller and reusable component that can be composed together. And that's a good candidate here. That's a code for the Play and Pause button. So let's encapsulate this logic into its own view. And we can call this PlayButton.
And now let's take a look at implementation of PlayButton. This is the same code as before just encapsulated into a new view. But notice that we made the new state here but state is not the right tool.
By using state, we've created another source of truth for isPlaying, which we have to keep in sync with the state in the parent PlayerView. And that's not what we want. What we want is to make this a reusable component. So this view shall not own a source of truth, it just need to be able to read a value and mutate it.
But it doesn't need to own state. And we have tools for this job. It's called binding. By using the Binding Property Wrapper, you define an explicit dependency to a source of truth without owning it.
Additionally, you don't need to provide an initial value because the binding can be derived from a state.
So let's see how does-- how this fits in our example. So the only change that we need to make is to use the binding property wrapper and omit the initial value. It's that simple. Let's now see how we can provide a binding into PlayButton by going back to our PlayerView.
PlayerView is still holding the state. That's your source of truth. From the state, you can derive a binding by using the dollar prefix on the property name. This is your way to allow a component to access your state via binding. The dollar prefix is another feature of property wrapper. And if you want to learn more, please watch the modern Swift API design talk.
I want for you to pause for a second and appreciate how simple but powerful this is.
PlayButton does not contain a copy of the isPlaying value.
Just as a reference to it through the binding, so there is no need to keep this data in sync between your view.
And I want to contrast this with what we have to write today in UI kit, graph kit.
You have a view controller that owns multiple view that need to respond to user interaction by manually setting up target action or defining a delegate.
You have to observe the model change and respond to those event too.
Every time any value changes, you have to read the value and set it everywhere it's needed. As soon as the complexity of your app grows welling up with a massive problem and I'm sure all of you know what I'm talking about here. The whole purpose of ViewController is to keep your data in sync with your view. This is all complexity that I-- you have to manage, but not in SwiftUI. You have a simple tool to define your data dependency and the framework takes care of all the rest. And well, you don't need a ViewController anymore.
This idea is so powerful. This is applied throughout our framework.
If you take a look at the API for component like Toggle, TextField and Slider, they all expect a binding. The framework keeps you in control of where the source of truth live. You create the data and give that to the component, a reference to it without ever duplicating that information or manually keep that in sync. And that's pretty amazing.
In SwiftUI views are so many app that can views for layout, navigation, and much more. And in fact they are your single composition primitive. They're also a great tool for encapsulating presentation logic for a single piece of data.
The frameworks allows you and encourage you to create small view to represent a single piece of data that can be composed together.
And this is, again, the framework guiding you to our composition of small unit that you can reason about. So let's go back to our example now.
I've shown this UI to my designer and she's very impressed by how little code I had to write.
But she suggested some improvement.
We should animate the transition within Play and Pause.
Fortunately, it's very easy for me to make my designer because the framework keeps track of everything that has changed. It's incredibly easy and powerful to drive an animation with state.
By wrapping the mutation to the binding with an animation block, the framework will animate the transition when the value changes. And you will always get the correct animation to the final state.
If you want to learn more about SwiftUI powerful animation and layout system and how to make amazing app, I invite you to watch Building Custom View in SwiftUI. We've now seen state and binding, but SwiftUI has quite few more trick up its sleeve. And to show you more, I want to invite Raj on stage. Raj? Thanks, Luca. I'm going to walk you through a few of the other tools we have from managing data in SwiftUI.
And by the end of this session, you'll be able to design and build robust reusable components that operate on all kinds of data. As you saw earlier, we have a number of powerful tools for managing data in SwiftUI. And Luca already covered a few of them such as using state, binding, or even just using a Swift property. I'm going to take you through the rest of these exciting tools starting with external changes in SwiftUI. So I want to bring back the diagram that Luca just showed you.
And in this diagram, the user interacted with the application. This created an action which resulted in a mutation to some state and that was driven into a new copy of the view that was rendered to the user.
Well, some events are initiated externally such as a timer or notification.
But remember, in SwiftUI, your views are a function of some state.
There's a single funnel point for all of your changes.
And what this means is that SwiftUI reacts to these external changes the same way it does to user interaction.
So when a timer fires or a notification is received, the process looks pretty much the same. We create an action, perform a mutation on some state, we get a new copy of the view and that's rerendered to the user. And in SwiftUI, we have a single abstraction for representing these external events.
And it's called a Publisher. The publisher comes from the new combined framework. Combine is a unified declarative API for processing values over time.
Now, we're not going to go into the details of Combine today, but you should definitely check out these related sessions where you can find out much more.
For our purpose, there is one thing to remember when using these publishers with SwiftUI, they should omit on the main thread.
Combine provides an easy to use operator called Receive On for this. For more information, you can check out the Combine and Practice session. OK. Let's walk through this with an example.
Now, our users can sometimes get lost listening to podcasts and they can get tired of hearing about the same millennial ramble on about avocado toast for hours on end.
So, we're going to add a time stamp to our podcast player so they know exactly where they are in the episode.
To do that, we'll add some state representing the current time and a text that draws that value. Next, we'll use the onReceive modifier. Conveniently, I already built a publisher that fires when the current time changes. And I'll use that publisher by passing it into this onReceive modifier. Additionally, I'll also provide a closure that will execute when the publisher omits.
And that's it. In doing this, we've described a dependency to SwiftUI.
So now, when the currentTime updates, we'll update our state and SwiftUI knows that there's a dependency there. So we'll update the label automatically. There's no manual invalidation or management here.
So we briefly covered external changes in SwiftUI.
Next, I want to talk to you about external data.
For that, we have the BindableObject Protocol.
BindableObject is a convenient way to use the well encapsulated tried and true model that you already have.
It's great for teaching SwiftUI about the reference type model you've already built.
This is data that you own and manage.
SwiftUI just needs to know how to react to changes in that data. So let's use another example.
Our users expect that their podcasts sync across all of their devices and I've been tasked with adding this functionality. So I've already gotten started and I've built up a model and now it's time to use the model that I've built in our view hierarchy and bring it into our podcast player. Let me show you how easy that is. You could see here a sketch of the model that we built. All I need to do to use this model with SwiftUI is to conform it to the BindableObject Protocol.
With BindableObject, all we need to provide is a publisher. This publisher represents changes to our data. And remember, the combined publisher is our single abstraction for representing external changes to SwiftUI.
So here, we'll provide a publisher in the didChange property. PassthroughSubject is a publisher. And then SwiftUI will subscribe to this publisher, so it knows when to update our view hierarchy. Then in the advanced method, when we mutate our model, we'll simply call send on the publisher.
Now, note that for correctness, we need to do this any time our model changes so that our view hierarchy can stay up to date. But thankfully, SwiftUI has our back. It handles these updates gracefully, so you get great performance and correctness.
So now we've built our model and its conformance to the BindableObject Protocol.
Next, I want to show you how to use that model in the view hierarchy.
Remember the two principles from earlier. Every piece of data has a source of truth and when you access that data, you create a dependency on it. So we've created our source of truth but we don't have a dependency yet.
Fortunately, it's really easy to create dependencies on your bindable objects. I brought up this very basic diagram here. You can see we have our view hierarchy in blue on the right and we have our model in green on the left.
Now, we can connect the two creating a dependency using the ObjectBinding property wrapper.
And as we do this, each view that has that property wrapper will depend on the model that we wrote earlier.
Just like with state, when you use the ObjectBinding property wrapper and add that to your view, the framework recognizes that there's a dependency there. And so in body, when you access that data, we automatically figure out when to update your view. In code, this looks something like this. When you create your view, you add the ObjectBinding property wrapper to a property in your view. And then when you instantiate your view, you just pass the reference to your model that you already have. Note that this creates an explicit dependency in the initializer of the view, which is really great because now anytime I go to instantiate my view, I know that it has a dependency on the model.
And that's it.
And as we do this, each view with the property wrapper will automatically subscribe the changes in our BindableObject, which means we get automatic dependency tracking. Again, no manual invalidation or synchronization needed.
So I want to pause and take careful note here because if using SwiftUI are value types, any time you're using a reference type, you should be using the ObjectBinding property wrapper. This way the framework will know when that data changes and can keep your view hierarchy up to date. So that's how to create a dependency using ObjectBinding on BindableObject.
But we actually have another tool for creating these dependencies as well.
You can create indirect dependencies.
So I brought back a similar diagram to the one that you just saw, but this time our view has gained some children. Next, I want to bring in the environment.
If you've watched the SwiftUI Essentials talk, you know that the environment is a really great encapsulation for pushing data down through your view hierarchy.
And using the environment object modifier, we can actually write our BindableObject into the environment. Now, our model is in the environment. We can create dependencies on our model by using the EnvironmentObject property wrapper. Now, by using this property wrapper, we can create a dependency on that model.
But there's more. You can actually use this in multiple places.
So you can use this in a variety of views throughout your hierarchy and they will all depend on the same model. And, of course, when that data changes, everything will get updated automatically.
You get the same automatic dependency tracking as you do with ObjectBinding. You describe the dependency to SwiftUI by using these tools and the framework handles the rest. It's pretty great. So let's update our podcast player to take advantage of this.
To do that, this is it. You just need to add the EnvironmentObject property wrapper to the view and then in an ancestor higher up in the hierarchy just provide the model using the EnvironmentObject modifier.
Now, whenever we use our player in the body, SwiftUI will automatically update our view on our behalf. So you're probably wondering when would I use EnvironmentObject versus ObjectBinding? Well, you can actually build your whole app with ObjectBinding, but it can get kind of cumbersome to pass around the model from hop to hop.
And that's where EnvironmentObject comes in.
It's a really useful convenience for passing data around your hierarchy indirectly.
Here, you can see by using EnvironmentObject, we can pass the model indirectly through our view hierarchy, which means we don't have to instantiate all the intermediate views to the hierarchy with the model.
So the environment is actually a great way to pass all sorts of data indirectly down through your view hierarchy. And you might have seen its use for things such as accent color or layout direction and even more.
And as Luca said earlier, data comes in all sorts of shapes and forms.
Well, values like accent color and layout direction, those are just data.
And when you use them in your views, you're creating a dependency on them.
In fact, the environment is a general purpose container for handling all sorts of indirect data and dependencies. And the framework uses it liberally to give you rich features like Dynamic Type and Dark Mode.
You can also use the environment in previews to provide new values for things like the accent color or theme.
So that's a quick tour through the powerful tools we have in SwiftUI for handling data.
Next, I want to help you build some intuition for how to use the right tool and how they all fit together. One of the big themes here is that every piece of data has a single source of truth. And in SwiftUI, we really have two options for managing these sources of truth. First, we have state.
State is great for data that's view local, a value type, managed, allocated and created by the framework. And BindableObject is great for data that you control.
It's great for representing external data to SwiftUI such as in onDevice database, this is storage that you manage, which makes it great for the model that you already have. So now that we've talked about sources of truth, I want to talk a little bit about building reusable components. One of the things that's great about SwiftUI is that views are low cost obstruction. And what that means is that you don't have to make the tradeoff between great architecture and performance. You can build the architecture you've always wanted to build and you can also get great performance. You don't have to make the straight off.
And with SwiftUI, you can focus on making your views into reusable components. When you do this, you'll probably notice that most of the time when you're using data in your views, you probably don't need to mutate it. And so, read-only access is preferred when you can get away with that.
For that, we have Swift properties and the environment. And because views in SwiftUI are value types, the framework can automatically determine when your data changes and update your view as a result. In general, you should prefer immutable access, but sometimes you do need to mutate a value.
And for that, we have binding. As Luca told you earlier, a binding is first class reference to data. It allows your components to read and write a value without owning it. And this makes it great for reusability.
In fact, you can get a binding to many different representations of data.
We showed you today how to get a binding to state, but you can also get a binding to an ObjectBinding. In fact, you can get a binding to another binding as well.
All you need to do is use the dollar sign prefix as we showed you earlier. It allows you to derive a binding from another one of these tools. I want to pause for a minute and appreciate how powerful this is. Earlier on, Luca showed you that many of the components we vend in SwiftUI, they operate on bindings.
So let's use Toggle for example.
Toggle takes a binding to a Boolean.
But the beauty of data in SwiftUI is that Toggle doesn't need to know or care where that Boolean lives or comes from. All it needs to do is know how to read the value and change the value.
Binding is a tool that encapsulates these operations which gets this separation of concern to the Toggle. And this is the real power of using data in SwiftUI. You can get great correctness and great separation of concerns.
So you'll notice, I didn't actually mention state when I was talking about building reusable components. Well, state is trapped inside of your view and its children.
So if your component needs to operate on a value that's external or somewhere else, state might not be a great fit. And state is a fantastic tool for prototyping a first approach, as you saw today with our podcast player.
But most of the time, your data is going to live outside SwiftUI.
For example, your data might live in a database and that will probably be represented by something like a BindableObject. So if you find yourself reaching for state, please take a step back and consider, does this data really need to be owned by this view? Perhaps the data, the state should be lifted up into a parent as Luca showed you earlier or maybe that data can just be represented by an external source using a BindableObject. So it's important to be careful when using state, but it does have its strengths.
One of the great uses of state that we have in our framework is button.
Button uses state to track whether the user is pressing on it and highlight appropriately.
And what's great about using state for button is that when you create a button, you don't need to care about the highlight state.
That is data that is truly owned by the button. So when you're reaching for state, consider do I have a case that's like button? And if you do, state might be a great tool. But if not, consider using one of the other powerful tools we've shown you for using data in SwiftUI.
So that's how to build reusable components with SwiftUI. But what we've shown you here is actually generally applicable to all sorts of software.
Every piece of software has data and every piece of software has data access.
And by carefully understanding your data, minimizing your sources of truth, and building reusable components, you can eliminate an entire class of bugs. And when you use SwiftUI, applying these concepts is incredibly easy because we've built them right into the framework.
We have a number of related sessions on SwiftUI and I encourage you to check out all of them. It will change the way you build apps. Thank you. [ Applause ]
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.