스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Introduction to SwiftUI
Explore the world of declarative-style programming: Discover how to build a fully-functioning SwiftUI app from scratch as we explain the benefits of writing declarative code and how SwiftUI and Xcode can combine forces to help you build great apps, faster.
리소스
관련 비디오
WWDC23
WWDC20
WWDC19
-
다운로드
Hello and welcome to WWDC.
Hi, everyone. I'm Jacob, and I'll be joined later by Kyle. We're so excited to show you SwiftUI, a great way to build better apps, faster. We think the best way to learn about SwiftUI is to see it in action by building an app. Using SwiftUI feels like magic, but to make it clear that I don't have anything up my sleeves, I want to go through the entire process of creating an app in SwiftUI starting from scratch. So, what kind of an app are we going to make? Let me set the stage for you. Kyle and I love to eat sandwiches. So we've been keeping a list of the best sandwiches we can find. And we want to make an app for it. Let's jump to Xcode and start building it. I'll start by creating a new project.
And using the multi-platform app template. And I'll call it "Sandwiches." And here's our new project. Xcode has started us off with everything we need for iOS and macOS versions of our app. We have a group for iOS-specific assets and a group for macOS-specific ones. But most of our code is in the shared group. We have a file here that has all of our app code. And this may seem too short to be the code that sets up our entire app, but this is all we need. We'll come back to this code a little later. For now, let's start with the code for our app's view.
The left side of our editor shows our code, and the right side shows a canvas with a visual representation of that code. In SwiftUI, view definitions are just Swift code, which means that the canvas and the code editor are just different ways of viewing and editing that same code. If we select something in the canvas, that selection is reflected in the code as well. And if you change something in the code...
that change is reflected in the canvas as well. They work together seamlessly so that you can move between them whenever you want. Let me tell you a little more about how it works. The canvas shows us previews of our view code, and it even helps us edit and learn about that code. Xcode shows these previews by compiling our real code and running it to generate a result. But one of my favorite things about previews is that they're also created using SwiftUI code. And later on, we'll see how that gives us a lot of power to customize our previews. Now, our app is going to show a list of sandwiches. So let's make the cell for our list. I'm going to add another piece of text under this one to show more information about each sandwich. And I'm going to add it from the library...
by just dragging it out onto my canvas. And Xcode even shows me what will happen when I drag it into different locations. When I drop it, the preview updates to show the text added, but even better, Xcode has actually edited my code to add that text. Xcode has embedded these views into a VStack to get the layout that I want. A VStack, or vertical stack, is one of the common layout containers in SwiftUI. It lets you stack views vertically. There's also an HStack that lets you stack views horizontally. And these stacks are containers. I can place any views that I want inside of them. I'm going to replace this placeholder with the ingredients of a sandwich. For now, let's just use a hard-coded value.
Next, let's add an image next to our text. I can make edits in the code just as easily as in the editor, so let's embed our view in an HStack right over here. I'll command-click on my view and choose "Embed in HStack." And Xcode has added the code to do just that. Now, I can just add an image next to our VStack.
We'll add some assets in a bit, but for now, I'll use an SF symbol image to get things up and running.
And already, we have a basic version of our cell. Now let's style the cell. I can command-click on a view and inspect it to see some properties about that view.
Let's change the alignment for our VStack.
And Xcode has updated my code to show that value. Now, let's inspect our ingredients text. This time, let's use the canvas.
Pro tip: You can control-option-click to go directly to the inspector.
I'll change to a smaller font. Let's use Subheadline.
One of the great things about Xcode editing my code is that it helps me to learn how to use SwiftUI. I can now see the code to set the font for some text. We call these kinds of methods "modifiers," and they're used in SwiftUI to customize the way your views look or behave. I'll add another modifier in code to set the foreground color to a secondary color. And now that I have my cell, let's put it into a list. To do that, I'll command-click on my cell and choose "Embed in List." This wraps my cell in a list and makes five iterations of that cell. And this code is all I need to show a list. No delegates or data sources, just views inside a list. Next, let's hook this up to some data. I'm going to drag in some assets and a model file that I created earlier.
My model has a few fields of information that we'll use. And to use this in a list in SwiftUI, I just need to make this type Identifiable.
This lets the list know when new items are coming and going. We already have an 'id' property, which is all we need. This model file also includes some test data that I can use for debugging my app. Now, let's go back to our view and pass in our data.
I'll add a property on my view for the sandwiches.
And one of the great things about previews is that they can use their own test data. So I'll just pass in our test data right here.
You may notice this banner that's appeared above our preview. When I make larger changes to my types, like adding the sandwich's property, Xcode pauses the previews until I'm ready to have them resume updating. I can click this button or press command-option-P to resume.
Next, let's use our data to drive the list. We'll pass the sandwiches to the list...
and we'll update our text to show the sandwich's name. And we'll show the correct number of ingredients.
And now that we have real images, let's use our sandwich's thumbnail for the image.
You may have noticed a subtle change in our cells. When we started, they were the standard 44 points tall. But when we changed to these larger images, the cells automatically expanded to make sure those images fit without any extra work. And now that I have those images in context, they look a little sharp. Let's apply a corner radius to our images using another modifier. If you're not sure what modifiers are available, you can view and filter a list of them in the Xcode library right here. We'll find the corner radius modifier, and we can drag it onto our canvas. Notice that for views that are in our list cell, Xcode even knows that these cells share the same definition so this modifier will apply to all of them. Let's drop it onto our image... and tweak the value.
Now that our cells and list are looking good, the next thing we need is to be able to tap on a cell to see more details about a sandwich. To support that, let's wrap our list in a NavigationView.
A NavigationView enables navigating between different parts of your app. On an iPhone, it shows a navigation bar And allows pushing onto a navigation stack. Let's also set the navigationTitle for our view to show "Sandwiches" in the bar.
Then I'll set up our cell to push onto the stack. To do that, we can wrap our cell's content in a NavigationLink.
A NavigationLink takes a destination to push. For now, we'll just use some text that shows the sandwich's name, and then we'll put our cell as the contents of the NavigationLink.
And you'll see that the UI has automatically updated to show these detail indicators in all of the cells. SwiftUI automatically handles details like this so that our UI looks right by default. Let's also check that the cell behaves correctly. And previews are great here as well. I can click the play button on my preview, which takes me live. This lets me interact with my real code right here in the canvas. So I can tap on cells to make sure that they push and pop as expected.
And if I swipe to pop, you'll notice an advanced behavior that SwiftUI has given me automatically. Our cell stays highlighted and interactively unhighlights as we swipe with no extra work.
One last change I'd like to make to this list is showing the number of sandwiches in a row in the list. But our view code is a little bit large now. And I don't want to end up with a single massive view. So let's factor the cell out to be its own view to get some separation of concerns. Xcode helps me do this in one simple operation. I can just command-click the view that I want... and choose "Extract Subview." All of the view code gets moved into this new view, and I even get to choose its name. Let's call it "SandwichCell." Then I'll just add a property for the sandwich...
and pass that sandwich in.
This is a fantastic workflow improvement. And with SwiftUI, views are very lightweight, so you don't have to worry about creating extra views to better encapsulate or separate your logic. Now that our list code is slimmed down, let's add the row with the number of sandwiches. Right now, we're using a single collection to drive our entire list, which is great for lists that are purely data-driven. But when I need more, SwiftUI also lets me mix static and dynamic content in lists and other containers. I can replace passing this collection to the list with a ForEach, which creates a view for each item in the collection.
Now I can add a static element right alongside this data-driven one. I'll just add another text below this ForEach and have it show the number of sandwiches.
And let's change its foreground color to be secondary as well.
Let's also show the text centered. To do that, we can embed the text in an HStack... and add some spacers.
A Spacer is a common layout element in SwiftUI. It behaves like a flexible space in a toolbar, expanding to fill whatever space is available. So these two spacers divide up any available space, which centers the text. Next, let's build our detail view. I'll create a new view using the SwiftUI View template... called "SandwichDetail." Xcode automatically gave me a view struct and the preview code to create it. I want this detail view to show more information about my sandwich, so I'll pass that in as an input.
And just like before, I use the preview code to set up a version of this view that uses our test data.
Now, to build our view, I'll just use an image with the sandwich's image name.
It's showing our image, but that image is too large for our view. By default, SwiftUI shows all images at the size of their contents to prevent visual artifacts from scaling the image up or down. But for photos like this one, we want to be able to resize them down. And we can use an image-specific resizable modifier to specify that.
Now it's the size of our screen, but I really want to maintain the image's original aspect ratio. I can do that with another modifier to set the aspect ratio.
And this lets me choose between "fill," which expands the image to take up its entire frame, or "fit," which makes sure that the image fits within the frame. And previews let me really easily see and understand the difference between these. For now, let's use "fit," so we can see our full image. Now, let's go back to our list and update our cell to push our new detail view when we tap on it.
We'll create our SandwichDetail...
and pass in the current sandwich.
I'll switch my preview back to live mode. And now I can tap on the cell to see my image. But now that I'm previewing it here, I can see that I forgot to set its title in the navigation bar. Let's go back to our detail view and fix that.
I'll just add the same navigationTitle here to set my title to be the sandwich's name.
But in the preview I have here, we're only seeing the view itself, and I'd really like to be able to quickly verify my change. Well, since previews have all the power of SwiftUI's views available, we can do just that. I can set up my preview to be in a NavigationView just like I would anywhere else in my SwiftUI code.
Now, my view's preview has a navigation bar, and I can see my title right there. Now, when I'm picking a good sandwich, there's one thing that's very important to me. The sandwich has to have just the right amount of sauce. No sauce, and it's too dry. Too much, and it's drowning in sauce. I can see that there's some sauce on this sandwich, but I want to make sure that it's not too much. Now, if this had an aspect ratio of "fill"... I could see the sandwich up close. Looks like a good one. What I'd really like is to be able to change back and forth between "fill," to see up close, and "fit," to see the whole sandwich. But how do I dynamically change this aspect ratio's content mode while the app is running? To understand how to do this, we really need to know more about how views work in SwiftUI and why. I'm going to turn things over to Kyle to talk about that. Thanks, Jacob. Hi. I'm Kyle, a member of the SwiftUI team. SwiftUI might be a little different from what you're used to, so, before we go any further, we're gonna step back and spend some time talking about the way views work. We left off implementing the SandwichDetail view.
Note that in SwiftUI, a view is a struct that conforms to the view protocol, rather than a class that inherits from a base class like UIView. This means your view doesn't inherit any stored properties. It's allocated on the stack, and it's passed by value. SandwichDetail just stores a sandwich, so it's the size and weight of a sandwich, no additional allocation or reference counting.
Behind the scenes, SwiftUI aggressively collapses your view hierarchy into an efficient data structure for rendering. Because of this, we make liberal use of small, single-purpose views in SwiftUI. And you should too.
What I want you to take away from this is that views are incredibly lightweight in SwiftUI. As Jacob mentioned earlier, you should never hesitate to re-factor your SwiftUI code because extracting a subview has virtually no runtime overhead. A view in SwiftUI and a view in a traditional UI framework fulfill the same primary role: they define a piece of UI. The view protocol only requires a single property: body. Which is itself a view.
You build bigger views by composing together smaller views. We built the SandwichDetail view by composing together Image, a view of an image at its native resolution... resizable, a view that stretches an image in either dimension... and aspectRatio, a view that proportionally scales its child. The rendering of any view you might build, like SandwichDetail, is just the rendering of its body. If you set a break point in the implementation of body, and the debugger stops there, its means the framework has decided it needs a fresh rendering of your view. Ta-da! The framework knows when to fetch a new rendering because in addition to defining a piece of UI, a view defines its dependencies. Let's extend SandwichDetail to allow the user to tap, to toggle between fitting into and filling up the available space.
The first thing we'll need is a state variable that says whether or not the image is zoomed. When SwiftUI sees a view with a state variable, it allocates persistent storage for that variable on the view's behalf.
If we decide to fill or fit based on that state variable, we've got a view that renders like this when it's zoomed and like this when it isn't. Now, all we need is a tap gesture to toggle back and forth between the two states. Then on tap, the image will zoom to fill... and shrink to fit.
So, what's actually happening here when we tap? One of the special properties of state variables is that SwiftUI can observe when they're read and written. Because SwiftUI knows that zoomed here was read in body, it knows that the view's rendering depends on it. Which means... when the variable changes, the framework is going to ask for the body again, using that new state value so it can refresh the rendering, this time with a different content mode. Traditional UI frameworks don't distinguish between state variables and plain old properties. However, I found the distinction to be incredibly clarifying. In SwiftUI, every possible state your UI might find itself in-- the offset of a scroll view, the highlightness of a button, the contents of a navigation stack-- is derived from an authoritative piece of data often called "a source of truth." Collectively, your state variables and your model constitute the source of truth for your entire app.
Earlier I mentioned that this call to aspectRatio makes a view. Its definition looks something like this, where contentMode is a plain old Swift property. You can neatly classify every property as either a source of truth or a derived value. The zoomed state variable is a source of truth. The contentMode property is derived from it. Recall, SwiftUI can observe when state variables are read and written. So when one changes, it knows which renderings to refresh.
The framework refreshes a rendering by asking for a new body, making a new aspectRatio view from scratch, thereby overriding the contentMode and any other stored properties.
This is the mechanism by which all derived values are kept up-to-date in SwiftUI.
We've seen that every state variable is a read-write source of truth... and that every plain old property is a read-only derived value. We're not going to see an example in this talk, but SwiftUI invents a tool called "binding" for passing read-write derived values. And technically, any constant can serve as a perfectly good read-only source of truth. The test data driving our previews is an example of this.
Lastly, I mentioned earlier that collectively, your state variables and your model constitute the source of truth of your entire app. Later on, we'll see Jacob use observable objects to teach SwiftUI how to observe changes to a model object.
Don't worry if the difference between these primitives isn't crystal clear to you yet. We've got an entire session dedicated to developing your instincts around when to use which of these data flow primitives. Okay. Let's just step back and take stock here. What we've seen is really different from what you do in a traditional UI framework, where the views themselves persist and you try your hardest to keep them all up-to-date and consistent. You may not think about it in these terms when you use a traditional UI framework, but every time a view reads a piece of data, it's creating an implicit dependency. It's a dependency because when that data changes, the view needs to update to reflect the new value.
When it fails to, that's a bug. SwiftUI automatically manages dependencies on your behalf, recomputing the appropriate derived values so this never happens again. Of course, we don't just manage a single dependency at a time. The UIs we work on are big and complicated. When it comes to how much you have to hold in your head and how easy it is to make a mistake, the way we manually manage dependencies today is really hard. Despite my best efforts, every update to every app I've ever shipped has had UI bugs. Every one of these lines is a dependency. And even after you understand all of them, you still have to make sure that your UI is in a consistent state across all possible orderings of event handler callbacks.
To clarify what I mean by that, we're going to look at a bug in an old version of the Sandwiches app, which was implemented in UIKit. Here's a sketch of the view controller code. When you zoomed in, it had a snazzy enhance button. So, if Jacob ever ended up with a low-resolution image like this one, he could still verify the sandwich included a healthy dose of sauce.
Tapping the button would dispatch a machine-learning operation on a background thread to enhance the image. Ah, that's better. I think I spy some spicy brown mustard. There was only one problem. We had a report of a stray activity indicator that never stopped spinning. The bug was caused by this unexpected ordering of events.
These kinds of mistakes are easy to make when you mutate your subviews directly in event handler callbacks rather than updating a source of truth and deriving your UI from that. This is because we can't help but code to the happy paths that come readily to mind and overlook the unhappy ones that don't. The problem is, as the number of events increases, the number of unhappy paths explodes. Assume we get all four events. How many different possible orderings are there? There are actually 24 different orders any four event handlers could be called in. In practice, it's even worse than this because each of these events can occur more than once. Say, for example, a user is mashing the enhance button. The challenge of managing this complexity should be familiar to anyone who has tried to juggle asynchronous callbacks or implement interruptible animations. These completion handlers can fire at all kinds of unexpected times.
If I could tell myself from five years ago one thing about my job, it would be that UI programming is hard. No one pretends synchronizing multi-threaded code is easy. It's taken me months to shake out the bugs in some of the multi-threaded code I've written. And even then, I couldn't be 100% confident in its correctness. A lot of UI code is actually just like that. I think we downplay how hard it is because it often only manifests as a view missing or in the wrong place. But we shouldn't. Race conditions and UI inconsistencies share the same underlying source of complexity – these easy-to-overlook orderings. Many of the views we all work on have to handle way more than four events. Model notifications, target-actions, delegate methods, lifecycle checkpoints, completion handlers-- they're all events. A view with 12 would roughly equate to 12 factorial possible orderings. That's almost half a billion. You can think about this as kind of like Big O notation for your brain. You're human. You can only fit so much in your head at a time. This dotted line? That's your app. What do you think the difference between these points is? That's right. Bugs. As we add features the number of possible orderings explodes, and the chance we overlook one increases to the point where bugs are inevitable.
I imagine many of you have discovered when using a traditional UI framework the simplicity that results from collecting all of your view updates into a single method. When you do this, you break the back of the curve we just saw, because when there's only one method, there's only one possible order it can be called in.
You may not have thought about it in this way, but this pattern forces you to define a source of truth for every possible state your UI might find itself in and derive your view's properties from that collective source of truth. If this sounds familiar, it's because SwiftUI was directly inspired by this best practice. We've codified it in the framework by making "body" the only entry point that is ever called. And in doing so, we've solved the tricky cases, the ones that I, at least, when using a traditional UI framework, was never able to fit into this pattern. Like removing subviews, pushing onto a navigation stack and performing updates to a table view. This is why Views, but also Apps and Scenes and any other SwiftUI abstractions with a body work the way they do. Because you're only human, and this pattern of simply fetching new instances for the parts of the UI that changed scales with your brain, virtually eliminating UI inconsistencies. Now let's get back to the demo and finish the SandwichDetail view. Jacob? Thanks, Kyle. To be able to look more closely at our sandwich, let's add a state property...
called "zoomed"... and default it to "false." And states should only be accessible within a view's implementation, so we'll make it private. Then we'll use it in our aspect ratio's content mode to change between "fill" when we're zoomed and "fit" otherwise.
And finally, we'll add a tap gesture to toggle our zoomed state.
Let's try it out in a live preview. Now we can change between these modes. But you might notice that when we're zoomed in, there's some blank space at the bottom. SwiftUI automatically lays out your views in what we call the safe area. This means that UI elements in your app won't get clipped by things like the corner radius. But for an edge-to-edge image like this, we actually want to expand to the whole screen.
To do that, we can just add a modifier... to ignore the safe area.
And specifically, we'll ignore it on the bottom edge.
Okay, we're close, but there's something missing here. This needs an animation, and with SwiftUI animations are really easy to add. I can just wrap my change in withAnimation...
and now it animates between its different states. And not only that, the animation is fully interactive and interruptible. I can tap on it at any time, and it always animates correctly.
I would add the enhance button next, but it turns out that the way Kyle trained the model, it only ever worked on that one image that he showed. So I'm going to add something more useful. Kyle loves spicy sandwiches, like this one.
But I don't, so I want to have a way to quickly know if a sandwich is spicy or not. Let's show an indicator for that below our detail view.
I'll add a VStack around our existing sandwich image.
And I'll move the more general modifiers to apply to that VStack. I want to show an image and text here, and a great way to do that is with a label.
A label takes a title to show-- we'll use "Spicy" and it also has an associated icon. We'll use a system image... called "flame.fill." The label shows the icon and title together for us. And it can also be used in other contexts, like lists and menus, where it will automatically take on the correct appearance, spacing and sizing.
I was kind of imagining a bottom banner appearance for this, where it's at the bottom of the screen with a background behind it. To move it down there we'll just add a spacer...
which will move the banner to the bottom and our image to the top. And to keep our image centered, let's add another spacer above the image.
Spacers automatically have a minimum size to maintain some padding between elements. But in this case, we want the image to be able to go all the way to the edges of its container. So let's set a minLength of zero for these.
Let's also add some padding in our banner so it still has some space when it's visible and doesn't go up against the edge of the screen. In the inspector... I can just click this button to turn on padding for this view. That's better. And let's turn up the font size as well.
Notice that not only did the text size increase, the symbol image did as well. Symbol images automatically use the same font information as text to size themselves appropriately. Let's use a headline font.
Now, to really make this screen spicy, let's give it a red background. A background modifier lets me put any view behind the view that it's applied to. These are commonly used with solid colors to give a solid color background to a view.
Well, we have red behind our view. But why is it just this small region? In SwiftUI, views size themselves to fit their content. So in this case the image and text are their natural sizes, and there's also space for the padding we applied. And just like earlier, we can make this expand edge-to-edge by adding spacers and an HStack.
Now, for a few finishing touches, let's turn the foreground color to be yellow to match our spicy theme...
and update our font to use small caps.
Looking good. Now we have our banner, but we only want it to appear when a sandwich is spicy. How do we do that? The declarative syntax we're using makes that really easy. We can just use an "if." We'll check if our sandwich is spicy... and if so, we'll show our banner.
To check this, we can change our preview data to show a different sandwich that isn't spicy. But even better, we can set up our previews to show multiple versions of our view. I can click the plus button to add another copy of this preview.
And I'll update the data we're using...
to show different sandwiches. Now, we can see one version of our view with a spicy banner and one without. That way, as we make edits, we can be sure that both versions of our view work the way we want. And notice that the way Xcode added another preview is just by adding another instantiation of our view.
I like this banner, but I don't want it taking away space from the sandwich image when we're zoomed in. So let's hide the image when we're zoomed, which we can do just by updating our condition.
Now the banner shows and hides as we zoom.
And it even animates... fading in and out. We can also customize that animation behavior by setting a different transition.
Let's use ".move" on the bottom edge.
Now it slides out and slides back in.
And if you look closely while I tap when the animation is still going...
notice that it turns around and comes back. No matter what I do, everything stays interactive and it always ends up in the right place. And that's our detail view. Let's review what we just built. Our detail view is configured with the sandwich to show. And remember, that's a derived value passed in by the parent of this view. We also have our state property for whether we're zoomed or not, which is persisted by the framework and controls our aspect ratio's content mode. And we have our banner, which is only visible for spicy sandwiches and only when they're not zoomed. We're also specifying a transition to make it slide in and out. And what's actually happening during that transition? When it's removed, the view is animating to a new position offscreen... and SwiftUI waits until it finishes that animation to actually remove the view from the hierarchy. And when it's coming back, SwiftUI inserts it offscreen and then moves it back in with an animation. It's pretty amazing to be able to add and remove views from a hierarchy with an animation so easily.
And recall that this animation is always interactive right out of the box. This is where being data-driven, instead of event-driven, really shines. All those events Kyle talked about can happen while this is animating too. And animations beginning and ending are even more events. It's incredibly difficult to build something like this in an event-driven world, but in SwiftUI it's just one line of code.
Now let's go back to our list of sandwiches and finish up this app.
When we started, we used a multi-platform app template, but so far we've only been looking at an iPhone. How much work do we need to do to run this on other platforms? Let's see. I'll switch my run destination from an iPhone to an iPad...
and go live.
SwiftUI has converted our navigation view into a split view, so I can choose sandwiches on the left... and show them on the right.
The one thing that I'm noticing in my preview is that when I don't have a sandwich selected, I just see a blank area. I'd like to improve that to show a placeholder saying to select a sandwich. All we have to do for that is to add a second view in our navigation view.
Just like you can add multiple views to stacks, you can add multiple views here.
But instead of those views being stacked, they're given to the navigation view to be shown in the most appropriate way. In this case, the first view is shown on the left and the second view becomes the placeholder for the view on the right. And on an iPhone the placeholder is automatically removed since it's not needed.
Let's look at what happens on macOS.
It's working great here too, and we get the same placeholder, shown here, as on iPad.
We're able to use the same view code, model code and app code between all Apple platforms. And we can make platform-specific improvements, like this placeholder, to go even further.
Over time, we need to be able to change our list of sandwiches. So let's add some editing support. And while we're at it, let's also make our data model a little more real. Right now, the data in our app is completely static. We have this array of sandwiches... and whatever we start with is what we'll always have. Let's update our model to have a root store object that will contain our sandwiches and will be able to change over time. I'm going to drag in a pre-built model file with our sandwich store.
And just so you know, that store as in data store, not a place that sells sandwiches.
Notice that our store is a mutable object that contains our sandwiches.
And we also have a singleton instance of that store for testing. Now all we need to do is tell SwiftUI when our object changes. To do that, I'm going to make it conform to the ObservableObject protocol. Then I can just mark any properties I want to observe with @Published.
So, how can we use our new model? Just like we used @State to make a source of truth for a value... we can use @StateObject to make a source of truth for a mutable object.
StateObject will automatically observe the object to update our view when it changes. And we could add that StateObject here in our view code. But since this is our app-wide store, there's an even better place to put it-- in our app code. Let's go back to our app code to look more closely and see how we can link it to our model.
This is the code we started with. And notice that it's very similar to the view code that we were just looking at. We have a struct that conforms to the app protocol... and it has a body property, where we build up what we want, just like a view. In this case we have a WindowGroup which lets us specify what view we want to use for all windows in our app. And one thing that's special about our app is that we also have this @main attribute. This just tells Swift that this struct should be the starting point for our app.
I'll add our store and a StateObject right here. Apps can use State, StateObject and other special properties just like views. Next, let's pass the store to our view code. We'll pass it to the view's initializer. And back in our view code...
we'll replace the constant sandwiches... with a property for our store.
Then we'll tell SwiftUI that we want to observe this object for changes by making it an ObservedObject.
And we'll update our list to pull the sandwiches from the store.
Finally, let's also update our preview to use our test store.
Great. Now we're pulling our data from the store. Which means we're ready to add our editing support. I'm going to drop in some convenience functions for making changes to our store from a snippet.
There's one to add a new sandwich... one to move sandwiches around and one to delete sandwiches.
In our lists ForEach, we can add an onMove modifier...
that calls our "moveSandwiches" method. And we'll also add onDelete...
to call "deleteSandwiches." And with just that change, we can go back to our app...
and we're already able to swipe to delete rows from our list. Whenever we swipe to delete, SwiftUI will call our callback...
which will remove the sandwich from the store.
And our UI will automatically update to show that change. On macOS, this is all we need for editing support. But on iOS, we should add a way to explicitly go into edit mode in addition to swipe to delete. So let's add an edit button as a toolbar item.
I can use a toolbar modifier...
which lets us add any SwiftUI views as toolbar items. Inside of it I'll just add an edit button, which is a control that automatically toggles edit mode. I only want this to appear on iOS, so I'll add "if os(iOS)" around that button...
so that it's only added to the toolbar there. Now let's toggle edit mode for our list. Notice that all of our data rows have editing controls, and the static element at the bottom does not. SwiftUI automatically shows the editing controls only on the rows that need them and omits them from the rows that don't. We can reorder items... and tap to delete them.
Let's also add a button for adding new sandwiches. I'll just add another view in our toolbar modifier, and for this one I'll just make it a button with a label of "Add"... and an action that calls our makeSandwich method.
Now we can tap our button... and there's our new sandwich.
Let's quickly review what we've just added. We saw how to quickly add editing operations to our list...
with just these modifiers... and some simple functions to change our data. And remember how we made our sandwich type identifiable earlier? ForEach automatically watches for changes to its collection and synthesizes the correct insertions and deletions for us so we no longer need to tell the list to add and remove rows, which means we no longer have to worry about getting data source inconsistency exceptions.
We also used a toolbar modifier...
to add toolbar items for editing our list and adding new items. And that's our list. We made this whole sophisticated list UI with just this really minimal view code.
We were able to build up this app really quickly. But you might be thinking that there's still a lot more work we need to get it ready for customers. These days support for Dynamic Type, Dark Mode, localization and more are expected for an app. But with SwiftUI you get a lot more support for these behaviors automatically. And we can use previews to really quickly test all of these. Let's go to our preview and take a look. I'm going to add a second preview by clicking the preview's "plus" button.
Then I can click this "inspect" button to configure the new preview. I'll set the Dynamic Type size to be a much larger value.
And everything looks great automatically. Let's look at the code that got added to change this preview.
Xcode just added a modifier that's setting a value in the environment of our previews. The environment is a way you can set contextual information about your views that flows down the view hierarchy and changes aspects of any contained views at once. It's great for making cascading changes to a view and its children.
Let's add another preview instance.
And this time, in our preview inspector... let's set the color scheme to "Dark." Once again, everything looks great automatically. And finally, let's see how our app works with other languages. I have some English string files... that I'll drop into our app.
Then I'll tell Xcode that we want to localize these files.
Then I'll go to my project file...
and import a localization into Arabic.
Now, back in our view code, let's add one more preview.
If we set the environment's layout direction...
to be "rightToLeft"...
everything just works already, which is great.
And finally, if we set the locale...
to be Arabic...
our app is localized. But even better, if you look back at our code... we didn't do anything special to support these features. To get our text localizable, we didn't have to mark up which strings should be localizable or not. SwiftUI automatically infers the text with string literals, like "sandwiches," should be localized by default.
But text that's created from strings, like our model values, should be used as is. And you can even use string interpolations and have them localized correctly. We're really excited for you to start building apps with SwiftUI. When you get all of these behaviors for free, you can concentrate on the unique parts of your app and build better apps for your customers even faster. Let's take one last pass through our app to review what we built and make sure everything is working right. Let's use the Dark Mode version and take our app live again. But this time let's do it on a device. I have an iPhone plugged in, so let's just click this button... to send our view to the device to preview it there. We have our list of sandwiches, and we can tap on one to see more information.
In our detail view, we can tap to zoom to full screen, which hides the "spicy" banner with a transition. And that animation is always interactive.
And we can edit our list to make changes.
Let's move this up.
And I'm a purist. I don't think a hot dog counts as a sandwich. And we can add our new sandwich. So there's our app. But there's one last thing that I want to point out, and it's something we didn't see. We just built up this entire application and tested all of these rich behaviors without ever once building and running our app. Xcode previews let us view, edit and debug our applications much faster than was ever possible before. Thank you for watching, and I hope you enjoy using SwiftUI as much as we do.
-
-
17:18 - Views are lightweight
struct SandwichDetail: View { let sandwich: Sandwich var body: some View { Image(sandwich.imageName) .resizable() .aspectRatio(contentMode: .fit) } }
-
18:30 - Views are composed
struct SandwichDetail: View { let sandwich: Sandwich var body: some View { Image(sandwich.imageName) .resizable() .aspectRatio(contentMode: .fit) } }
-
19:52 - View are dynamic
struct SandwichDetail: View { let sandwich: Sandwich @State private var zoomed = false var body: some View { Image(sandwich.imageName) .resizable() .aspectRatio(contentMode: zoomed ? .fill : .fit) .onTapGesture { zoomed.toggle() } } }
-
21:40 - Where is truth?
struct SandwichDetail: View { let sandwich: Sandwich @State private var zoomed = false var body: some View { Image(sandwich.imageName) .resizable() .aspectRatio(contentMode: zoomed ? .fill : .fit) .onTapGesture { zoomed.toggle() } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.