스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
SwiftUI Essentials
Take your first deep-dive into building an app with SwiftUI. Learn about Views and how they work. From basic controls to sophisticated containers like lists and navigation stacks, SwiftUI enables the creation of great user interfaces, faster and more easily. See how basic controls like Button are both simple yet versatile. Discover how to compose these pieces into larger, full-featured user interfaces that facilitate building great apps with SwiftUI. Build your SwiftUI skills as you learn the essentials of Apple's new declarative framework.
리소스
관련 비디오
WWDC21
WWDC20
WWDC19
-
다운로드
Good morning. Good morning. And welcome to SwiftUI Essentials. My name is Matt Ricketson and I work on SwiftUI and later I'll be joined by my colleague Taylor. So what do you all think of SwiftUI so far? Me too. I'm incredibly excited to talk to you today about SwiftUI. Now we have a lot to cover in this session, so let's dive right in.
SwiftUI is a new framework that is designed to give you the shortest path to building a great app. And that means giving you the shortest path to building great user interfaces. But even though SwiftUI is a new framework, a lot of it will already look familiar to you. And that's because it has all of the basic components that you'd expect from a UI framework. It has controls like buttons and text fields. It has layout containers like stacks and lists. It has drawing, animations and gestures. And SwiftUI even embraces platform-specific concepts like menus on the Mac, the Digital Crown on Apple Watch, and the Siri remote on Apple TV. And so the takeaway here is that we're not trying to reinvent the wheel with SwiftUI.
But as we all know, the reality is that just knowing how to use these kinds of components is not what it takes to build a great app, because a great app also needs to account for these kinds of things. It needs to be accessible and work with features like dynamic type. It needs to adapt to different devices and screen sizes and input types. And it needs to come alive with things like interactive animations and support for system features like Dark Mode and Drag and Drop.
These are the kinds of things that help your app to reach the largest possible audience and also help keep it feeling modern. Now we all know that even this though is not the whole picture, because of course you also add in your own unique features that make your apps stand out from the crowd. So I just want to take a moment to step back and acknowledge that this is a lot of stuff to have to learn. It's a lot of stuff to have to code and maintain, and so how can SwiftUI help you with all this? Well, think about your own apps for a moment. First, you have those basic features that everyone expects from your app, like controls and navigation, being accessible and adapting your layout to different devices.
We need to do these things and we need to do them right in order to build a really great app.
But then there are those exciting custom features that are unique to your app. And these are also the fun features, the features that we pour our passion into, the features that make us feel proud of what we've built.
And so the goal of SwiftUI is pretty simple: we want you to spend as much of your time as possible on that fun stuff and less time on the basic stuff, but without compromising on quality. And this is what we mean by giving you the shortest path to a great app. Because all of you are building great apps already. We just want to help you get there a little bit faster.
This session is about giving you a better understanding of SwiftUI. We're going to look at some code, but we're also going to talk about SwiftUI's design and how it helps you build better apps. By the end of this session, you'll be able to build a complete user interface with SwiftUI. And we're going to start by covering the basics of views and modifiers. And for that we'll need an example. And I always try to pick an example that I care about to help motivate me.
Now if any of you have been on the internet lately, you've probably read about what Millennials like myself consider to be the most important part of our lives.
That's right, avocado toast. We've got some Millennials in the audience. So today we're going to build an app for ordering avocado toast. And I've already done a little bit of work on it already and it looks a little like this. It's a simple form that lets me quickly order just what I want right from my phone. Now this is not much so far, clearly, but we're going to build on this throughout the talk. But before we dive into the code, I want to talk a little bit about views. And that's because views are the basic building blocks of user interfaces. And they're important to everything that we do in SwiftUI.
If you've ever used another UI framework before like UIKit or AppKit, you've probably already heard of the term view.
SwiftUI also has views and they serve the same primary role as they do in those frameworks.
Which is that at a high level, a view is just something that defines a piece of your UI. When you look at an app, everything that you see is defined by a view.
Individual controls are views.
The containers holding them are also views. And in fact, every single pixel that you see onscreen can be traced back in some way to a view. And we build user interfaces by composing these views into a hierarchy of containment. From the containers at the root, to the text, images and shapes that are at the bottom.
Now if you've used UIKit or AppKit before, this picture should look familiar to you. And the important thing to understand is that this is also true of views in SwiftUI. Where SwiftUI may be different than what you're used to is in the way that views are expressed in code. So let's look at some code. In our example app, we just have a vertical stack of controls and text. And it's easy to see that just by reading the code.
But in fact you'll notice how closely the code on the left matches the equivalent view hierarchy diagram on the right.
We see that in the stack at the root, to the text and controls contained in the stack. To the individual text labels contained in each of our controls.
Now what you don't see is calls to functions like Add subviews anywhere.
Because instead of building up our view hierarchy piece by piece, we initialize it as a complete, composed structure.
This is because SwiftUI defines its views declaratively as opposed to imperatively. And I can't think of a better analogy to help explain these concepts than of course with avocado toast. So let's try making avocado toast imperatively.
Imperative code involves building a result by sending explicit commands. That's sort of like teaching a friend how to make avocado toast over the phone.
You start by telling them what ingredients to get and what equipment they'll need, then you start guiding them through making the toast and cutting the avocado and all these instructions start getting a little tedious. And if your friend messes up any little step like forgetting to toast the bread, then the final result is ruined.
Now let's compare that to making avocado toast declaratively.
Declarative code involves building a result by describing what you want but letting someone else figure out how to make it for you. That's sort of like ordering avocado toast from an avocado artisan.
Luckily, we have a lot of those in California.
Now all you have to do is say exactly what you want. You can even throw in a custom instruction. And that's all there is to it. And because an expert is making it for us, we're guaranteed to get a high-quality result. Now going back to our code, SwiftUI is serving that role of the expert ready to assist you. In code, we declare the hierarchical relationships between our views by initializing a structure that encodes those relationships.
And SwiftUI does the hard work of translating your views into a rendered result onscreen.
Now there's a lot more to say about that, but for now let's just get used to the syntax in the code. And we'll start with container views.
container views are declared as a composition of other views serving as their content.
Those Content views are declared within a special kind of closer known as a view builder.
For example, we already saw VStack or Vertical Stack which is an example of one of these containers. view Builders allow us to write declarative code in the body of the closure. Instead of calling a function like AddSubViews, we can just list out our contents within the closure. To see a little bit more about how this works, let's take a look at the actual API for VStack. You can see the content parameter defined as a closure but marked with this ViewBuilder attribute.
The Swift Compiler knows how to translate a closure marked by this attribute into a new closure that returns a single view representing all of the contents within our stack.
This is an example of SwiftUI using the power of Swift to help you write less code.
views like VStack can also take other parameters in addition to their content. For example, we could configure our VStack to align its content along its leading edge instead of using the default center alignment.
Taken together, this is a really nice and natural syntax that lets us use braces and indentation to differentiate our container views and their configuration from the contents inside of them.
And we also follow the syntax for many controls since most controls in SwiftUI are also containers.
You saw this in our example app. In each case here, our controls define a piece of text serving as their label which describes their purpose.
Now we can put more than just text here. We can put any kind of view. And we'll go into more depth on that later in the talk.
Now another kind of syntax you see here are those dollar signs preceding the arguments to our Toggles and stepper.
The leading dollar sign indicates that we're passing a binding to the control instead of just a normal value.
So what are bindings? In our example app, our stepper is contained within a view that depends on persistent state to track the current order. It declares a property for its order using a state attribute. When SwiftUI sees a property marked with this attribute, it automatically creates and manages persistent state behind the scenes and then exposes the value of that state through this property.
In this case, our state contains a struct that I defined myself that represents all of our order information.
If we just want to read or write to the data in our state, it's really easy. We can just read or write to a property directly.
And we did that here when we made the label for our stepper.
However, a stepper also needs to be able to edit the state when its buttons are tapped.
And we use this dollar sign prefix to indicate that we should pass a binding to that Quantity Property in our state instead of just passing a read-only value.
A binding is a kind of managed reference that allows one view to edit the state of another view.
Now to learn more about state and bindings and how to manage all other kinds of data dependencies that you'll use in your app, I highly recommend that you watch the Data Flow Through SwiftUI talk. But for now, the important thing to remember is that if you ever see a property attribute like state that usually represents some kind of data dependency that SwiftUI is managing on your behalf behind the scenes.
And if you ever see a dollar sign prefix, that usually means that we're passing a binding to another view.
Now going back to our example app, there's one more important piece of syntax that we haven't covered yet. And you can see it up there at the top where we set the font for our title.
Let's zoom in on that.
First we initialized our text, which again is just another kind of view in SwiftUI.
Then we called a method on the text named font and passed it a system-defined text style. This kind of method is known as a modifier in SwiftUI. And a modifier is just a method that creates a new view from an existing view.
Let's see what I mean. This is what our UI would have looked like without the font modifier, in which case our title would have rendered with just the default body font.
This is what the view hierarchy diagram looks like. We just see our text contained by our VStack.
When the text is modified, a new view is inserted that wraps our existing text. The new view tells SwiftUI to render that text with its new font.
These modifiers can even be chained together. For example, we could change the text color of our title by adding a foreground color modifier.
This adds another view into the view Tree that wraps our font modifier view.
Now clearly our view hierarchy is starting to get bigger pretty quickly. And for the experienced UI programmers among you, this may be setting off some internal alarm bells. Because over the years we've trained ourselves to optimize the performance of our apps by keeping our view hierarchies as small and light as possible. But remember, we're writing declarative code.
And SwiftUI is our expert chef taking our views and skillfully producing a rendered result according to just what we ordered. And so even though we had to wrap our text in multiple wrapper views, SwiftUI collapses that down behind the scenes into an efficient data structure that is then used by the render system.
And without having to worry about the performance impact, you'll find that this chaining modifier syntax actually provides a lot of really nice benefits.
For example, modifier chains enforce a deterministic ordering of visual effects.
So here we have a piece of text with a green background. But the text is looking a little cramped, so let's try expanding that background by adding some padding around our text.
So we added the padding modifier and you can see it adding a new view to our view hierarchy. But nothing changed onscreen.
In fact, the padding is there; we just can't see it.
Looking at the code, our background modifier is only wrapping our text, not our padding. Which means that the padding gets applied outside of our background.
And luckily, it's really easy to fix this by just moving that background modifier to wrap both the text and the padding instead.
Now let's take a step back and appreciate what we just did there.
Imagine if padding and background were properties on our text instead of separate modifiers.
In that case, we would have no way to know which order they get applied in without trial and error or reading documentation.
Instead, by chaining modifiers together like this, we make that order explicit. And we also make it super easy to customize like we just did.
Now another benefit of these modifiers is that they can be shared across views. For example, here we've applied an opacity effect to multiple different kinds of controls.
And we can even apply that opacity to the entire stack instead of each individual control. None of these views had to define their own opacity property. Which means that they're free to have simpler, more focused interfaces of their own. And this gets at a general principle of SwiftUI.
Which is to prefer smaller, single-purpose views.
These kinds of simpler views are easier to understand and also easier to maintain over time.
And once you have all of these little views, you can compose them together to create bigger, more complex views.
The entire SwiftUI framework is oriented around composition of small pieces and you should organize your code in the same way.
So you can start with something simple like our text.
You can modify that into something better. And you can compose that together to build something great. You know, like an app for avocado toast. And personally I can't wait to see the kinds of user interfaces that all of you are going to build with SwiftUI.
But before you can do that, we're first going to need to know how to build our own custom views. And so let's build something new now.
Looking at our app, I'd really love to be able to see a history of my previous orders.
I've already sketched out a design. It's just a simple list showing a summary of each order and some icons for the toppings that I chose to include.
I've already gotten started on the code, so let's just go through this quickly step by step.
First, I declared a new view called OrderHistory as a struct that conforms to the view protocol.
We'll come back to that. My view has a single input property, previousOrders, which is just a collection of all of my order information.
My view has a computed property called body returning the contents of the view. And the sum keyword that we use here is a Swift feature that lets Swift infer our return type automatically.
Our body property returns a list which generates its contents by mapping each of our previous orders into a collection of new views, one for each order using another one of those trailing ViewBuilders.
So now that we understand this code, let's go back and take a deeper dive and learn why SwiftUI defines custom views in this way.
And let's start with how views are structs that conform to the view protocol. If you're coming from UIKit or AppKitt, you've probably gotten used to views being defined as classes that inherit from a common view superclass instead of as structs conforming to protocols. For example, custom views in UIKit inherit from the UIView superclass. And UIView defines storage for common view properties like alpha and backgroundColor. Let's imagine we built our OrderHistory using UIKit instead of SwiftUI. Our Custom View would inherit the stored properties of UIView as well as adding more properties for its own custom behavior.
So how is SwiftUI different than this? Well, remember that in SwiftUI we represent those same kinds of common view properties as separate modifiers instead, like we did for opacity and background. And each of these modifiers creates their own view.
And this means that the storage for those properties is distributed across our view hierarchy in each of these modifier views instead of being inherited by every individual view.
Now this allows our views to be lighter weight, optimizing their storage for just their unique purpose.
And in this world, it makes a lot of sense that view just becomes a protocol because it's no longer needing to serve a common storage template for all of your views. But what does this view protocol actually do? Well, let's remember our conceptual definition of a view. Which is that a view defines a piece of our UI and we build bigger views by composing together smaller views. And that's all that the view protocol does. It defines a piece of our view hierarchy, giving it a name so that it can be composed and reused across your entire app. And each concrete type of view is just an encapsulation of some other view representing its contents in its body property and all of the inputs required to create that view represented by its properties.
Now the actual protocol just defines that one body property returning just another kind of view.
But looking at this definition for a second, some of you may be asking yourselves, isn't that kind of recursive? If I have some view and it defines as body as another kind of view, well, then that view is going to define its body as another kind of view. And it has to end somewhere, right? It can't just go on forever.
So the reason this works is because SwiftUI provides many kinds of primitive views, meaning views that don't have any contents of their own and that represent those atomic building blocks on which all other views are built.
We've already seen text. An image is another example of a primitive view.
SwiftUI also offers primitives for drawing like Color and Shape, as well as layout primitives like Spacer. In fact, you can do some pretty sophisticated drawing just using primitive views in SwiftUI. And to learn more about that, you should definitely watch Building Custom Views in SwiftUI talk.
Our example uses text. But our list actually adds in its own primitive views that you can see as the dividers in between each of our rows.
Now we also saw that our Custom View is defined as a struct instead of a class. And this goes back to how views are defined declaratively in SwiftUI.
In this case, that means our views are not persistent objects that we update over time using imperative event-based code.
Instead, our views are defined declaratively as a function of their inputs.
So whenever one of our inputs changes, SwiftUI will call our body property again to fetch an updated version of our view.
Now List that we're using here -- List is a great example of the power of declarative code. If our previousOrders collection changes, SwiftUI will compare the old and new versions of our list and efficiently update the rendered result onscreen just based on what's changed.
For example, I've been working on cloud sync for my app. And it's really important to me that all of my avocado toast data is available on all of my devices. So let's see what happens if another device starts adding and removing orders from our history.
What you see on the right is SwiftUI automatically diffing the changes in our collection and synthesizing insertions and deletions and then rendering them with appropriate default animations. And this is all functionality that you get for free without writing any additional code. It's pretty awesome. And the reason this works is because you don't have to manage that persistent render state yourselves. Instead, you can just generate new values for your view based on your current data in that body property.
And you can let SwiftUI generate the necessary changes between those two versions on your behalf.
And that's the power of declarative code.
So let's build out the rest of our orderHistory view. And if you recall, our original design included these icons for any extra toppings that I included in my order, like salt and red pepper flakes. So let's start by showing that icon for salt. First, we'll add a horizontal stack with a Spacer after our text.
And then I'll show my SaltIcon view but only if our order contains salt.
As you can see in the code here, that ViewBuilder syntax that we talked about earlier, it lets us use natural control flow like if statements to declaratively define when a view should be included in our stack.
And using if statements like this in our declarative code feels really natural. But there are also other ways to write conditional code within your views. And it's important to choose the right tool to get the correct result onscreen. So let's look at a quick example to see what I mean. I built another screen for our app which lets you choose between a normal and flipped AppIcon.
And my first pass at this was writing a custom view that takes a flipped state as an input and conditionally applies a rotation modifier based on my state.
However, this produces an ugly crossfade animation when we actually try to flip that icon. This is because our code is telling SwiftUI to switch between two different kinds of views. A view wrapped in that rotation modifier versus our AppIcon just by itself. And by default, SwiftUI fades in and out views when they're added and removed. Which is why we get this crossfade effect.
Now instead I'd really like that icon to rotate when it's flipped. And so to do that, I define a single view with a single rotationEffect modifier and conditonalize its input based on our state. By defining our condition inside of our modifier, SwiftUI can provide a better default animation, rotating our icon to the new orientation.
And the lesson here is that you should try to push your conditions into your modifiers as much as possible. Because that will help SwiftUI detect those changes and give you better animations.
That if statement syntax that we saw earlier, that's really great if your intention is to actually add or remove views from your hierarchy.
So going back to our example app, our orderHistory view is starting to get a little bit big. So it would be nice to start breaking this down into some smaller pieces. So let's try factoring out the code for each List row into its own custom view.
First, I'm going to create a new custom view called OrderCell. Now I'll need a body for this view, and luckily we've pretty much already built that just within our lists in our OrderHistory view. So let's move that code over.
Our OrderCell requires input data in order to generate its body. So we're also going to need to add a property to represent that.
And finally, we'll finish up by creating an instance of our new view for each row within our list.
And the takeaway here is that it's really easy to break down your UI into smaller pieces and to factor out code into new views. And remember, with declarative code, adding a new wrapper view is effectively free since SwiftUI will optimize it down behind the scenes. And so the important thing here is that you no longer have to compromise between organizing your view code the way that makes the most sense to you and getting the best performance from your app. So let's finish by including that final icon for red pepper flakes. And it's easy to do that just by adding another condition like we did before.
Now this works but it doesn't seem very scalable. If we add new toppings in the future, we'll have to add them with new conditions into our code. What would be really great instead would be to conditionally generate a collection of icons from our order data.
To generate a collection of views, we can use a ForEach view.
Just like our List, ForEach takes a collection of data and a ViewBuilder that maps each data item into its own view.
But unlike List, ForEach doesn't add any visual effects of its own. Instead, it just adds its own contents to its container.
So this code is a lot better because now our order history will automatically support new toppings in the future without us having to add any more code to our view.
For example, we could add a third icon for eggs.
So taking a step back, it's pretty amazing how much functionality we were able to just build with just about a dozen or so lines of code.
And what's even more amazing is all of the code that we didn't have to write.
We already saw how SwiftUI automatically handled changing data, even inserting default animations when our data is added and removed.
But I didn't mention that our app also adapts to dynamic type.
And it even supports Dark Mode. And we got all of this support for free without writing any additional code.
This is pretty great and this is what we mean by SwiftUI giving you that shorter path to a great app.
So that's a lesson on building custom views with SwiftUI.
And now I'd like to invite up my colleague Taylor to talk to you about how to take full advantage of the views that SwiftUI provides for you out of the box. Thanks. Thank you, Matt.
Hello, everybody. At this point, we have a pretty good start to our app, with Matt building out the initial order form and history screens. But one thing that stands out is that this doesn't quite look like iOS apps we're used to. They're usually not these simple vertical stacks of controls. And typically, this type of UI looks something more like you'd see on the right. And one of the biggest differences is the container around the controls themselves having this standardized group list style. Now in SwiftUI we refer to this as a form. And a form is a container just like VStack, but one built specifically for building these sections of heterogeneous controls, giving the overall result a standard look and feel no matter what the platform.
Now we've already defined the exact set of functionalities we want in our app. The title, Toggles, stepper and button.
And all we're doing is changing the container itself from the existing VStack into a form. And then we can easily add in some sections to divide up that content.
Now just as Matt previously discussed, our code continues to reflect the resulting UI. And since the core definition of our controls didn't change, our code really didn't have to either. Just by changing the container from a VStack to a form resulted in the controls automatically adapting to that context. From the overall background and scrollability to the lines separating each of the controls, to even the styling of things like button. This is yet again SwiftUI taking care of the details for what exactly it takes to render those elements, and allowing us to focus on the functionality of our app.
Now one subtle change that happened isn't visible from this static screenshot. Focusing on the buttons, you can see that the alignment, padding and decoration has all changed around the button, but the press state has even taken on the special full bleed effect that you would expect from this type of UI, all the while showing the same exact definition of being a button.
Like you might expect, this same definition works in other contexts or other platforms, having a wide variety of possible looks and feel. button also demonstrates the same inherent ability for composability that we've seen in other views. The label is of course not constrained to just being a text but could also be an image. It really could be any type of view that we could define, even an explicit vertical stack of an image and a text. And this inherent composability enables a wide variety of possibilities while at the same time enabling button to be distilled down to two fundamental properties. The action it performs when activated and the label describing what that action is.
And that's the entire API surface of button. This is of course not to say that these are the only two ways that buttons can be customized. Like we saw before and will continue to see, both context and modifiers enable adding many more rich behaviors from disabled state to the styling of the button to even control sizes on macOS. But this core definition plus adaptive behaviors enables any type of button. And over time and across the different platforms, we've seen a lot of different buttons. Not only did they vary based on how they look but also in how we interact with them, from a click to a tap, to being selected using the switch control or the Siri Remote, but they can all be distilled down to having an action and a label.
Now just like button, every control in SwiftUI carries the same ability to have this adaptive behavior.
Controls describe the purpose or the role that they serve instead of just how they look. And this allows them to be reused across these different contexts and platforms and adapt to those situations. And this also helps them have that smaller API surface catered to that exact role. And at the same time still having fewer controls rather than need a control for every context you might need to use it in.
And all the while still enabling really powerful customization such as completely redefining how buttons should look in your app. Now we saw how this adaptivity allowed us to quickly transform from a simple stack of controls into the standard look and feel of a system form. But this same adaptivity also enables us to take these controls to other platforms such as the Watch, so we can quickly order our toast on the go.
Now the other control we're already using is Toggle. And you've already seen how Toggle in SwiftUI is more than just a literal switch. And this is true regardless of the platform it's on. And like button, Toggle has two fundamental properties, whether it's on or off, and the label describing the overall purpose of the Toggle. And again, that's reflected in the construction itself.
Now one notable difference from button is that it doesn't take an action, but instead takes a binding to a Boolean value. And this binding is a direct read/write connection to some piece of state or model in your application and allows the Toggle to reflect and update that without manually needing to respond to an action, pull the value out and then set it in your model. It takes care of it all for yourself.
Now Toggle and the other controls are also adaptive in one other very important way. For some people, UI's are a visual experience while others might predominantly use their other senses to experience that exact same UI. For instance, people with impaired vision are able to use VoiceOver to navigate and interact with your app using audio. And for those of you who haven't heard it, this is what it sounds like to begin using VoiceOver. VoiceOver On.
Now VoiceOver is just one of the system-wide features that are able to take your UI and surface it in these alternate forms. And because Toggle and the other controls are defined based on their purpose and include that human interpretable label, they can automatically adapt for these features. So when we navigate to this Toggle using VoiceOver -- Include Salt, Switch button, On Double-Tap to Toggle Setting. It is able to reflect that same label. And this is true even when the label isn't text. Now for images, if the image name isn't descriptive enough, you can explicitly provide a label directly alongside the image.
And of course even for completely custom -- It's really exciting, yeah. And of course even for completely custom views, you can always explicitly provide the label using the accessibility label modifier.
Now in addition to VoiceOver, this information also admits use for other features, like the new Voice Control on iOS and macOS so that we can say, "Tap Include Salt," and our UI behaves as we expect.
And making sure your app is accessible means it will work with all these different technologies and means that everyone can use your app. And SwiftUI is here to help.
There's a great talk this year that will go into a lot more detail about how you can make sure that your SwiftUI app is fully accessible.
Now at this point we've been able to quickly build up this initial basic interface that has all the behaviors we expect: dynamic type, Dark Mode and accessibility. But we've really only added a few customization options for the toast itself. And of course everyone knows that a professional artisanal toast repertoire comes with a variety of different bread types, methods to prepare the avocado and of course a variety of spreads and add-ons. To add in these more advanced configuration options, we can look for some inspiration from the flexibility that is macOS. Or we might want to have a little utility window to allow us to order toast right from our desk.
You can see here that the existing controls we're already using take on the expected look for macOS -- the Toggles, the stepper, the button. But we also have a few additional controls that allow us to pick from the type of bread, the spread to add, and how to prepare the avocado.
Now these are all examples of the Picker control in SwiftUI. Picker is built for the purpose of selecting one value out of a set of options.
Now Picker is obviously a little more complicated than the other controls and in fact has three core properties instead of two.
The options that you can pick from, the current selection from those options and the label describing the overall purpose of the Picker. Now the selection is a binding, just like Toggles is on property. Which allows us to directly connect it again to our modeler state. And the type of this binding corresponds to the tag values associated with each of these options. When one of the options is selected, that tag value is written back into the selection and back into our model, all with no work.
Now of course Pickers on macOS don't always manifest as pop-up buttons. In this single window, we can see two different styles of Picker, both a pop-up button and a radio group. While SwiftUI automatically provides a default style that's adaptive to where controls are used, controls also inherently have the ability to customize their styling, both to system-provided styles and even custom-built ones. In this case, we want to override the default style and impose an explicit radio group since we know that we are only picking from two options.
Now we can consider doing the same for our spreads.
But what might start out as a humble set of four possible spreads could quickly grow into a wide variety. So when it comes to building our Picker, we obviously wouldn't want to splay out each of these options one by one, just as we wouldn't want to build a UI that displays them all as radio buttons.
We've already seen using ForEach to build data-driven views. And since each of these options are views themselves, we can use it here as well.
This is a lot better.
Here we're going through each of the cases of spread and creating a new option with the spread's name and the spread itself as the tag.
Now -- Now obviously Pickers exist on more than just macOS. And then isolation -- a Picker on iOS looks like the traditional wheel-style Picker. However, since we're building up a form, SwiftUI will automatically adapt Picker to take on another really common style of this type of UI.
Here we can see that the spread Picker is now represented by a navigation row displaying both its label and currently selected value. Tapping on that row brings us to a list of all of our options. And tapping one of those selects it and brings us back.
You stole my punch line. This is SwiftUI taking care and creating that entire interaction just with our simple creation of a Picker. Making it trivial to build out the rest of our three Pickers. And just like in macOS, we still have explicit control over the ultimate style. If we wanted a wheel-style Picker here, we could again just impose that.
Now we have a pretty nice set of apps at this point. But it's one thing to order toast at our desk or while on the go, and it's another thing entirely to have heated debates with friends and family about what exactly makes the best avocado toast.
The form on the right side consists of the same content that we saw in the other apps and taking a look at the code that's used to build it, it's not a surprise that it's using the same structure and control creation that we used before. And again, the difference is that automatic adaptation. For instance, Toggle being represented using on/off buttons instead of switches.
And this gets to the heart of something really important across all of SwiftUI. The idea that you can learn a concept once and apply it anywhere.
SwiftUI is not just a means to write once and run anywhere, but it's a framework that enables you to learn these core concepts and use them in a variety of different contexts and platforms. This scales from the modifiers and ViewBuilder syntax to the shared core types like color, image and ForEach, to even these higher-level controls. One example that really illustrates to me this reuse of knowledge is a slightly platform-specific example of building a contextMenu. The contextMenu itself can be attached to an associated view using a modifier. And this modifier uses the ViewBuilder syntax to define its menu contents. Now if we take a look at the menu, we can see a few familiar concepts. Some elements that on click perform an action and have a label describing that action, and others that specifically get turned on and off. So it's not a surprise that the contents themselves are built up using the same controls we've already learned how to use. buttons, dividers and Toggles. But still, automatically taking on the expected look and feel for our macOS menu, from the hover and accelerated gesture handling, to the special highlight and selection styling.
From these few examples, you can already tell that controls in SwiftUI are a little bit special. They're defined based on their purpose, the role that they serve, their connection to your app's model, rather than specifically to their visual appearance. And this means that they're inherently reusable across a variety of historic contexts, and the appropriate look and feel can be determined based on that context, platform or other information. And at the same time, they're customizable, both in their use of views as labels and options as well as being able to arbitrarily style these controls from the system styles like you saw with Picker to even completely custom-built styles. And no matter what the style, still having accessibility support built right in.
Now earlier Matt showed a few examples of using modifiers to impose additional behavior on views. And the same is true for controls as well.
One example that those of you on iOS will already be familiar with is changing the tint or accent color for your UI, which affects how many different system controls appear. And if we want to apply this to our entire app, we can apply the accentColor modifier to our outermost view and it will be inherited by the entire hierarchy such as this button. Now when it comes to disabling controls, we can use the disabled modifier. For instance, disabling the Order button when maybe there are no toasts being ordered. But there also might be scenarios when we need to disable entire groups of controls. For instance, when we're unable to connect to the toast network to even place our order, we probably want to disable each and every control in our form. But this looks a little tedious and error-prone if we ever add additional controls. But like you saw with modifiers in general, we can instead lift this modifier up and apply the modifier to our entire form, just like we did with the accentColor modifier.
Now all the controls in our form will be disabled based on this single statement. And all of this adaptivity and inherited behavior is pretty powerful and potentially comes as a surprise since we're using these simple value-type views. But let's take a little look under the hood for how some of this works. These examples are built on top of something called the environment. And the environment consists of all the context for where your views appear in.
These are things that you might have previously thought of as being shared global state, part of our trait collection or properties on your view, or maybe even had to reach up to some ancestor object to pull the value out. But now this is all packaged up into the environment. And it's accessible to any of you that might want to access it. And each view inherits that environment from its parent.
Now as an example, when running in an Arabic locale, the environment at the root of our app has a right-to-left layout direction. And every view inherits that layout direction. But at any given point, the environment can also be overridden for a subtree of views. So if we were building up some media playback controls, we'd want to ensure that they're laid out left-to-right. And so by using the environment modifier, we can impose that on that hierarchy.
Now the environment is also one of the important technologies that helps make previews so powerful. It enables showing the same exact UI in a variety of these different contexts so we can really preview our app against all the ways people might be using them.
Now you've seen how the environment automatically affects various system views, and custom views are able to use the environment as well. So I've been working on a little control for our next update, which allows deciding exactly where on top of our toast an egg should be placed. You can see it's built up using a simple ZStack of two images: a toast on the bottom and an image being positioned with a dragGesture on top. With that, we can tap and drag the egg into just the right spot.
Now if we go to use our Egg View, there may be some cases we need to disable it. Maybe the shop ran out of eggs.
But since we're using a system dragGesture, it will automatically be disabled by the disabled modifier. So if somebody comes in and tries to drag that egg, it won't budge.
Of course, we should also offer some visual feedback that it's disabled as well, and thankfully that's pretty easy.
We can add an environment property that's connected to the isEnabled value from the environment. And we can use its value just like any other property. For instance, reducing the saturation of our overall construction when it's disabled.
And if the egg placement view ever becomes no longer disabled, SwiftUI will automatically recall our view's body and re-render it to the now undisabled state. And again, this is SwiftUI automatically managing our dependencies on the environment so we can just express our view's relationship to it and not have to worry about observing for when things change.
Now we've covered a number of controls and how to compose those all together. But we're still missing one really important piece of every app, and that's navigating between these screens, from the order form to the egg placement Picker to the order history. Now let's start in with the order form. Now a problem that some of you might have already noticed is the look of the title in the form. It doesn't use the standard navigation bar styling. So we can first wrap our Orderform in a NavigationView as the content of our app. NavigationView provides the ability to navigate through screens of our app revealing more nested or detailed information. On iOS, NavigationView also adds in the standard navigation bar Chrome. And then we can use the NavigationBarTitle modifier to produce that large beautiful title for our form. Now this modifier is a little bit special. It provides information that's able to be interpreted by a NavigationView ancestor. We saw earlier examples of modifiers that have information flow down the view hierarchy using the environment, and this is an example of one that flows information upwards using something called preferences. Now we're not going to go into too much detail on that, but you'll see other similar examples later.
So focusing on the form, the next thing we want to do is add support for including an egg in our order. So we can add a little Toggle here and then whenever somebody opts into including an egg, we can add a navigation row which takes us to our EggLocationPicker. So let's expand out the form to see how this works. It's built using a Toggle bound to whether or not our order includes an egg. And then it uses the same ViewBuilder conditional that Matt showed us earlier to optionally include that navigation row. Now the really cool thing is that we provided an animated binding to the Toggle. So whenever somebody taps that switch, our navigation row will be animatedly inserted in for the formList just with the setup.
And expressing the navigation row is also amazingly simple. It's using a specialized control called a Navigationbutton which allows us to provide some destination content to navigate to when interacted. Navigationbutton automatically comes with all of the right look and feel such as the disclosure indicator on the trailing edge. Now because views are lightweight, we don't have to worry about having created the EggLocationPicker here. SwiftUI takes care to only render these views once they're actually presented. Now inside the EggLocationPicker we can use our PlacementView, customize the navigation bar so that once it's presented, the title reflects its current state. We could also add a trailing BarItem to quickly reset the egg back to its start state. Like you hopefully expect at this point, the items here are the same views we've already learned how to use, so we can just provide a button. And that's all it takes to create this complete navigation experience.
Now we can turn our attention to the OrderHistory. Now we want to navigate to this, but it isn't more detailed or nested information of the form, but it's instead an entirely different section of our app.
This is more appropriate for the use of a TabbedView.
As such, we can wrap our form in a TabbedView just like we did NavigationView and then add the OrderHistory as another child.
Both have tabItemLabel modifiers that it described to the TabbedView how to label them in the TabBar.
Now we can quickly jump over to our OrderHistory. But at this point we've a pretty simple level of detail for the OrderHistory and we might want to expand this into a much more detailed set of information that we navigate to from our history list. This is another case of nesting or showing more detailed information like we saw earlier with NavigationView and button. So we can replace the contents of our OrderHistory list so instead of it being in list with the OrderDetail displayed inline, we can instead use this new OrderDetail as the destination for our NavigationButtons. And really it's this simple to build a data-driven list that's able to navigate to additional content.
This works great on the iPhone but if we take a look at the iPad, we want this to be set up using a master detail with a SplitView. Unlike NavigationStacks on iPhone that push onto a single RootView, here we know we have two points of navigation: the Master which is able to push content onto the Detail. So while our NavigationView behaved correctly with just the single RootContent on iPhone, we want to indicate that it intrinsically has these two pieces of content: the OrderHistory Master and the DetailView. Here we can use an OrderDetailPlaceholder View to act as the placeholder for when nothing is selected. Now with this, when a Navigationbutton is interacted with in the OrderHistory, it will automatically get pushed onto the OrderDetail. This will behave as we expect on the iPad and other wide-size classes using a SplitView. And for narrow-size classes, will automatically collapse into a single NavigationStack. And of course, this works on macOS as well, resulting in a SplitView there. And this isn't really write once and run anywhere; there are still these additional design considerations such as the increased information density on macOS.
But SwiftUI is automatically taking care of a base level of platform look and feel from how the SplitView behaves to the height of the table rows, et cetera. So that we can learn how to use these different concepts once and then apply them anywhere. And then we can focus our time on those exciting and custom features that make each of your apps great.
Now we've covered a reasonable amount of breadth in this last hour and there are a number of other talks that go into a lot more detail. We showed how state and bindings will change how you interact with controls, but data flow in SwiftUI will make you rethink altogether about data-driven UI updates.
We built up a few custom views using layout adjusters, but Custom Controls in SwiftUI will go into a deep dive on advanced used of layout, graphics and animations and has the most awesome demo.
We know that many of you are going to be eager to jump into SwiftUI right away and might be wondering if you can integrate this into your existing app. And the good news is yes, SwiftUI is designed to be integrated seamlessly alongside your existing views and models. And we have an entire talk showing you how to do that.
We touched upon how SwiftUI is designed to make your app accessible to everyone out of the box. Of course, there will always be some additional considerations and this talk will go into additional detail. And finally, last but certainly not least, we've shown how SwiftUI raises the bar for how much you can share across platforms. SwiftUI on all devices takes that as a baseline and goes into additional detail on how you can make a great app on any platform. There are a few additional talks such as WatchOS Specifics for more details on what's driving this and What's New in Swift. And finally, thank all of you for watching. We are so excited. [ Applause ]
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.