Learn how to build custom views and controls in SwiftUI with advanced composition, layout, graphics, and animation. See a demo of a high performance, animatable control and watch it made step by step in code. Gain a deeper understanding of the layout system of SwiftUI.
Hi everybody. It's so great to be with you all again at WWDC.
I'm Dave. If you've seen the other sessions on SwiftUI, you know how easy it is to assemble all the parts of an application and get it working.
Today, John and I are going to show you how to go from creating a functional app to one with fine-tuned layout, beautiful graphics, and some really cool animations.
We're going to present two SwiftUI subsystems and then John's going to use them together to build a custom control. So let's get started. Now from the moment you get started with SwiftUI you're already experiencing the layout system. So that blue box you see around the text in your preview editor, that's its bounds and layout's all about deciding the bounds of things on the screen.
But let's take a look under the hood and see what's happening with this example.
Technically, there are three views at work here. There's the text at the bottom of the view hierarchy. Your content view which always has the same bounds as its body, the text.
And finally, the root view which in this case has the dimensions of the device minus the safe area insets. So if you see something like this at the top of your phone, for example, it's not included. Pro tip, you can still lay stuff out in that area by using this modifier, okay? But by default you're in the safety zone.
Now, the top layer of any view with a body is always what we call layout neutral. So its bounds are defined by the bounds of its body. They act the same. So you can really treat them as the same view for the purposes of layout.
So there are really only two views of interest here and the layout process has three steps.
First, the root view offers the text a proposed size and that's represented by those two big wide arrows. And because it's at the root, it offers the size of the whole safe area.
Next, the text replies, well that's mighty generous of you but I'm really, I'm only this big. And in SwiftUI there's no way to force a size on your child, the parent has to respect that choice. And now the root view says alrighty, I need to put you somewhere so I'm putting you in the middle.
So that's it. This was a simple example but every layout interaction plays out in the same way between parents and children. And the behavior of your whole layout emerges from these parent child interactions.
But I want to highlight the second step because it's different from what you might be used to, and it has an important consequence for you.
It means that your views have sizing behavior.
Since every view controls its own size, it means when you build a view, you get to decide how and when it resizes.
For example, this view is a non-negotiable 50 by 10 points by virtue of the fixed size frame at its root.
And this one is flexible but the height and width are always the same. So it's always got a one to one aspect ratio. So sizing is encapsulated in the view definition.
We also saw this in action with text.
So in SwiftUI, the bounds of text never stretch beyond the height and width of its displayed lines, and we'll see why that's important when we talk about stacks in a minute. Now, there's one final step in layout that's crucial for getting UIs to look good and you don't really have to think about it in SwiftUI because we take care of it, but it's worth knowing that SwiftUI rounds the corners of your view to the nearest pixel. So instead of anti-aliasing like this, you always get crisp, clear edges.
So this is just one of many details every great app needs to get right. But SwiftUI takes care of-- just takes out of your way. Yeah. So you can focus on the things that make your app special. Okay, now that we have the basics in hand, let's see if we can't make things a little more appetizing.
I'll change the text in our example to something random like, I don't know, avocado toast.
Are you hungry yet? No? Okay. Let me try to make it a little bit more appealing. I'll add a nice green background here.
Now, this background modifier wraps the text view in a background view with the color view as a secondary child.
So now the green background exactly matches the bounds of the text.
So pro tip number two, throwing a background or border color on a view is a really useful trick if you want to observe the view's bounds and you don't have a preview canvas handy.
Okay, now I want a little bit more space around the text in the edge of the green box so I'm going to insert some padding there.
Now, SwiftUI chooses an amount of padding that's appropriate to our platform, dynamic type size, and environment.
And when you don't pass any parameters, you get adaptive padding in exactly the same way that SwiftUI adaptively styles a picker or a button depending on the context it's in.
And if we want to adaptively pad just the leading and trailing edges, well we can do that too. Adaptive modifiers are the best way to adjust your layout because you avoid complicating the code, wasting time on details early in your process, and hard coding constants that might be inappropriate elsewhere. But since we're here to get control over details, let's say we have a specification that calls for exactly 10 points of padding on all sides.
Okay you can write that explicitly.
Now this example's a little more interesting than hello world. So let's see how the layout process works in this case.
First, the root view proposes its entire size to the background view.
And much like our toast view itself, the background view is layout neutral so it's just going to pass that size proposal along to the padding view.
Now, the padding view knows it's going to add 10 points on a side to its child so it offers that much less to its child-- the text-- and the text takes the width it needs and returns that to the padding view which knows it should be bigger than its child by 10 points on each side and it situates the text appropriately in its coordinate space.
Now, we said the background view was layout neutral so it's just going to report that size upwards. But before it does, it offers that size to its secondary child, the color. Now, colors are very compliant when it comes to layout. The accept the size offered to them. So the color of the size is just the same as that of the padding view.
Finally, the background reports its size to the root view and the root view centers it as before. And that's the whole process.
Ready for another example? This one's even simpler but it's important.
So in this case the view's body is just a fixed size 20 by 20 image.
In SwiftUI, unless you mark an image as resizable, either in the asset catalog or in code, it's fixed sized.
Now, I'd like our view, our whole view to be about half again as big so let's add a 30 by 30 frame modifier like this.
Now, you might have noticed that the image, though undeniably appetizing, has not changed its size. But that shouldn't be surprising should it? We said it was fixed sized. Around it you'll find a 30 by 30 frame and that's the size of the body of our view. So the view we've defined is in fact 50 percent bigger than it was before we added the modifier.
So, is it a contradiction that the size of the frame doesn't match the size of our image? Actually, no.
This is the layout system doing what it's supposed to do.
See it's important to recognize that the frame is not a constraint in SwiftUI, it's just a view which you can think of like a picture frame.
It proposes fixed dimensions for its child, but like every other view, the child ultimately chooses its own size.
So in that sense, SwiftUI layout uses a lighter touch than you might be used to.
The payoff, though, is that there are no underconstrained or overconstrained systems in SwiftUI which means everything you can express has a well-defined effect.
So there's no such thing as an incorrect layout unless you don't like the result you're getting.
Okay, now that we have the basics under our belts, let's discuss the power tools-- the stacks.
Now, HStack and VStack arrange their children in a row or column respectively.
I threw this list cell together with four stacks and just a few lines of code. And here's the code for that layout.
At the top level you got an HStack with two children, the first of which is a VStack containing the star rating. And the other child is also a VStack that leading aligns its two children, the first of which is yet another HStack containing the title, a stretchy spacer, and our avocado image.
So there you go.
Four stacks. Let's put it back together.
Now-- So I want you to notice that SwiftUI didn't slam all of the stacks children against each other. It left some space between these two because adaptive spacing is in effect.
You'll also find that the baseline to baseline spacing for adjacent text exactly matches Apple's human interface guidelines, as does the baseline to edge spacing. Because we've encoded these rules into SwiftUI's layout system, the general principle here is that the simplest, easiest code is also well on its way to producing a beautiful result.
But, if you need control, as always, SwiftUI has your back with knobs you can turn to get exactly the result you want.
Oh, I almost forgot another thing that SwiftUI handles for you. If your app is localized for a right to left writing system like Arabic and you change the system language, well SwiftUI flips your horizontal coordinates for you so you don't have to recode the layout. So if you've been wondering why we talk about leading and trailing alignments instead of left and right, now you know. It's so that your layouts are automatically ready for internationalization.
Okay, so let's get an inside look at stack layouts.
Now, most views we've looked at so far have effectively been a linear chain of children. But stacks are interesting because the children have to compete for space on an equal footing.
In this stack we've said the text shouldn't wrap to more than one line, which means that if the stack is asked to fit in less space, well the text will just be truncated to fit.
But let's start with the case where there's plenty of space offered by the parent, okay? So first the stack figures out the internal spacing requirements and subtracts that from the proposed width to give us an amount of unallocated space. And now we have three children whose size we don't know.
So we divide the remaining space in three equal parts and then we propose one of those as the size for the least flexible child.
Now, we said the image was fixed sized right? So that's the least flexible.
So this-- the image takes this much, and whatever size it claimed, we deduct that from the unallocated space. And repeat.
Okay? We now have two unsized children so we divide the remaining proposed size in two and offer one half to the least flexible child that doesn't have a size which is delicious.
So delicious claims this much, which you can see is less than the offer. Remember that. And again, that is deducted from the unallocated space and that leave this much room for avocado toast which, as you can see, is plenty.
Okay, last steps.
Now that all of the children have sizes, the stack lines them up with the spacing from earlier.
And since the code didn't specify an alignment, the default of centering is in effect.
So the stack uses center alignment to arrange the centers of all of the children vertically.
Finally, the stack chooses its own size so that it exactly encloses the children. Now, if you think about it, you might be able to visualize why the bounds of text don't stretch beyond their displayed width.
See if delicious had accepted all of its offer, that would leave less for avocado toast which would have forced it to truncate despite the fact that everything could have fit, there was plenty of room. Actually, of the two pieces of text here, avocado toast is clearly the more important, right? It's the subject.
Delicious is just an adjective. It'd kind of expendable.
So this wouldn't be a good result.
But, now that I think about it, that means the amount of space- when the amount of space offered is less than the ideal, the truncation behavior we saw earlier probably isn't quite what we want either.
With a narrow offer like this, we'd rather keep all of the subject intact and let the adjective truncate. Okay, to achieve that we have another power tool. We can raise the layout priority of avocado toast from the default of zero, to one. So when children in a stack have different layout priorities, the stack takes the unallocated space, it sets aside the minimum widths of all of the lower priority children, and then it divides the rest among children with the highest priority value.
So in this case there's just one of those, it's avocado toast. And so that'll be offered all of the available space minus the widths of the image and the three dots that remain after shrinking delicious to its minimum.
Okay, after sizing all of the children with the highest layout priority, the stack moves on to divide the remaining space among children with the next highest layout priority, and so on.
Okay, there's one last power tool I want to show you.
Now, I know you won't be surprised that we can bottom align this stack. Looks pretty good that way right? But, consider what happens when we change the font of delicious to something smaller. Well, it looks okay to me, but what do I know? I'm just an app developer.
I'm pretty sure Crusty, my UI designer is going to have a problem with it though. Yeah he's going to zoom all the way in and start picking at the details like this.
Bucky, he'll say, first you got the baseline of delicious which is here and then you got the bottom of the image down here. And then there's the baseline of avocado toast way up here, and none of them line up. What have I taught you? Fortunately we have an answer for that in SwiftUI.
The first and last text-based line positions are alignments just like top and bottom. So if we align the text on its baseline, it solves the problem neatly. But what about the image? See the image has no text in it but every alignment has a default value and the default value for last text baseline is just the bottom edge of the view, so that gets me exactly what I'm thinking Crusty will ask me for.
Oh. Oh. Looking again, I'm pretty sure-- I got a bad feeling about this-- I'm pretty sure he's going to tell me there's a visual baseline that's up here, 87.4% of the way to the bottom. We can handle that by telling SwiftUI how to compute a last text baseline for the image in terms of its other alignments. Pretty cool, right? But we're not done with the power of alignments yet. Let's go back to our nested stack example.
Suppose we wanted to align the center of these stars and the title, like this.
Now remember the text in question is nested in two different branches of the view hierarchy.
So, well center aligning the children of that top level HStack isn't going to cut it because that's the default, right? And so we're already there. You can see those children are already aligned. Now what we need is an alignment other than center that marks the middle of these stars and of the title.
We need to define our own alignment which is actually this easy. It's just six lines that we put in an extension on vertical alignment.
So first we define an enum conforming to alignment ID which has one requirement, tell SwiftUI how to compute the default value.
Now, it doesn't really matter what we choose here in this case because defaults aren't going to project out of those inner stacks. But I defined this default to be bottom just so that you could see that it's just like defining an alignment guide modifier.
And last of all, we define a static instance of vertical alignment that takes the enum type as its argument.
And now we can use it to align the stack, explicitly setting it to the center of the stars and of the title.
Now the explicit alignment values we've set, they project all the way out through two layers of nested stack allowing the outer HStack to align those inner parts.
So that's custom alignments. A power tool that will help you satisfy even the most persnickety UI designer.
Now, I'd like to invite John Harper to the stage to tell you about graphical effects in SwiftUI. John.
Thanks Dave. So yeah I'm going to talk about, describe some of the graphics features of SwiftUI and how you can use them to create interactive controls in your applications. So here's an example of the kind of thing we'd like to create potentially. You know you've seen this before but, you know, it's several normal controls but then in the center there's this big kind of graphical ring with a gradient around it and at the bottom there's a bar chart. And so you know if you wanted to do this in your apps you'd have to dive into the graphics system to some degree. You know core animation or core graphics. And so we think we've got a good way to do things like this in SwiftUI and so we're going to look at a little example. But before we can do anything complex we need to cover the basics. So if we wanted to draw a red circle, how would we do that? Well we'd first of all we'd create a custom view type because you know everything is a view.
And then we'd put something like this in there. And here we're saying if you give me a shape and the color we can fill it with-- fill those two things together and get, you know, a red circle on the screen. But there's something really interesting here which is that we didn't give it a positional size and that's because we're relying on the layout system, all that stuff Dave described, to kind of position our views for us even though we're in this drawing model. And so, you know, shapes will just kind of react to the layout system and produce views. And in fact, you know now that our draw views, if you think of them that way are really just views, then that really means that everything in SwiftUI applies to drawing because everything in SwiftUI is views. And so all the modifiers you've seen about layout and animation, filter effects-- you know, just everything-- it all applies to drawing in the same way it applies to views. But similarly we've added a lot of kind of new custom modifiers purely for graphics. Things like blurs and shadows. But because drawing is just views, they all apply to regular views as well as the graphics views. And so we think this unification of kind of normal control like views and graphics is going to be incredibly powerful as we go forwards. Okay but let's look at this a little more. So the basic pattern is that we have a shape and a style-- kind of a color or something-- and the combination of those two things produces a view. As we have a few shapes here and as we saw I can fill with red and get a red circle.
But also we could use a different operation and say a different shape-- you know a capsule-- and say stroke it with red. And in that case we won't get a filled shape, we'll get a filled outline of the shape. And that's often what we want, but you know sometimes we find we might want a slightly different kind of stroke, so we could also say stroke the border of the shape rather than the kind of on the shape. And that's just a variant. And this is also showing that, you know, all of these stroke operations all can either just take a line width as in the first example, or effectively all of the standard stroking parameters like dashes and end caps and the line joints, that you've probably seen in other graphs APIs.
Okay so we've seen the shapes and we've seen filling them. But to this point we've only been using colors. But there are actually other things we can use to fill shapes with. You know we can use tiled images images and we can use various kinds of gradients to kind of fill the shape with.
So here's an example of using a gradient and all of the gradient styles all use this base kind of type which is just providing the one dimensional color ramp. And in this case we've given it seven colors. It'll equally space them along the continuum and that just gives us our ramp. Once we have that, we can pick one of those gradient styles. You know, in this case we're going to use an angular gradient. We give it the color ramp and then we, in this case, for the angular gradient we give it the center point and a starting angle. And then it can just fill the colors around that circle effectively pushing them to infinity to give us our kind of color fill. But obviously then we can take the style we just made and apply it to a circle, in this case doing a fill as we saw before. And now we've got, instead of a red circle we've got this nice kind of color wheel.
But of course filling is just one of our operations so we could equally have just said stroke border and got a filled ring.
Okay so that's the basics and we've seen drawing individual things, but now we want to go on and build something a little more complex out of multiple drawing operations, multiple views. And so this is the example we're going to use for the rest of the session. It's actually a sample code you can download and it's just kind of an interactive pie chart kind of thing and it's made up of a bunch of color wedges where you can add them and remove them. They can animate in and out.
Okay, so before we can figure out how to draw it, we need to look at the data. And so our sample app is providing us a data model and it's very simple. It's just a clasp representing one of these kind of wedge-- ring of wedge things. And each wedge is a few properties representing the geometry and the color of the view. And then we've got a dictionary of the wedges tracked by ID and then finally an array of the IDs just so we know which order to draw them in.
And so we can go on and think about how we're going to draw this now.
Just as we saw before, we really want this to interact with the layout system so we're going to assume that there's a layout bounds for the entire control. You know because we want it to resize and move around as expected. And if you think about how this will work, you know, we can draw each of these colored things separately. And as long as they all fit within that same layout bounds, we can then composite them together and they'll all align seamlessly.
So that really means we can just think about one of these.
And so, you know, we've seen things like this before. It's really some shape filled with some gradient.
In this case, a shape kind of like this.
But we don't actually have this shape in the toolkit of the built in shapes in SwiftUI, but that's not a problem. We can go on and define a custom shape for this.
So custom shapes are really like custom views in that they're, you know, types conforming to a protocol. Except in this case we're not conforming to a view protocol, we're using the shape protocol. And the shape protocol has a single requirement which is this path and rect function. And the rect you see here is the layout bounds or the frame of reference I guess. And then it's returning bézier path.
So for our custom shape we'll give it a single property which is the wedge description, the thing containing the geometry. And then additionally we'll just create an empty path because we're going to put some things in that and then return it later.
Just to kind of simplify this a little bit, I would also use a helper tiles which effectively abstracts some of the geometry of the shape we're drawing just to kind of hide the sines and cosines because, you know, this is circles and things like that. But this is really just defining some variables, you know, that we can then use in the rest of this function. So first of all we'll add the-- one of the inner arcs.
Then we can add a line to the path joining the inner and outer rings.
And then another arc to kind of wind back around the circle.
And finally we can just tell the path, hey close this current sub path which will join the last point to the start point, and we've got our shape. Excuse me.
So now we can go on and actually draw this shape. We could fill it with our gradient and that's most of the way there.
There is one thing we still need to do though. We saw in the movie that we'd like the shapes to animate. And if we just use this shape as it is, there is no way that SwiftUI can animate this because it doesn't know enough about the types. And so we can go on and add one extra property to our custom shape called animatable data and this is really providing a vector of floating point numbers that the system is able to interpolate. In this case we're going to delegate the responsibility for this to data model because it's implemented this property for us. But really it's very simple. It's taking the three properties and the wedge description and then combining them into one value that can be interpolated.
Okay so now our shape really is complete, we can go up and draw it.
And so we can get back to thinking about how we layer together our diagram.
So as we saw we can get a gradient, an angular gradient again and just fill our shape. But that's one of them. We really want, you know, eight of these things.
And composite them together. And so we can do that with something called a Zstack which is very similar to the H and Vstacks Dave was talking about but it layers things together kind of depth-wise versus spatially.
So we'll create another custom view and this time our view will grab the data model via the environment. We've set that up somewhere else. And we'll start off by creating the Zstack.
Now we saw on our data model that the, you know, it's giving this array of wedge IDs. And so we can use that with the ForEach view in SwiftUI. And really what that's doing is mapping over that array of IDs and effectively creating one view for each wedge we want to create.
And so this wedge view thing is very simple. It's really just a single statement view that, you know, creates the custom shape, fills it with a gradient. Okay so this is fairly complete now. You know, because of how SwiftUI tracks dependencies, when our data model updates the view will update, you know, because Zstack handles insertion and removal transitions. They will fade in and out cleanly and that's pretty good for an application like this. But there are a few more things we can go on and do now. You know we saw in the movie that we'd like the wedges to be deleted when you tap on them.
So we can add something called a tap action and this is kind of like an event handler for the view and it's really saying if you tap inside this view's shape, then run the closure. And in our case the closure we're going to use is enable animations and then ask the data model, hey remove this wedge ID. So once we've done that we can tap on the wedges. Now there's one other final thing we'd like to do here. I said that the default transition is a fade in fade out effect and that's great for a lot of things but in our case we want something a little more fun so we're going to scale the wedges down towards the center as well as fading them.
And we can do that by adding a custom transition called scale and fade. And this is something we're going to have to create for ourselves.
Okay, so we think about what this transition's going to do. You know we want the-- when the views are added, we want them to start off scaled down and faded out, and as they, you know, kind of animate in, they'll come in, scale up to the normal state. While they're sitting in the view hierarchy they'll just kind of sit there and then finally they'll reverse that transition to kind of get removed.
And so if you think about that, you know we don't need to define all those frames obviously because we have an animation system. And so we defined the end states the animation system will take care of the rest.
But of course in this case there's actually a symmetric transition here so we only actually have two end states. We have the transition kind of set up and remove state and we have the normal state where nothing is really going on. So now we know what states to define we can define them in code. And the way we do that in SwiftUI is with something called a view modifier. Now a view modifier is a little bit like a view in that it's defining some piece of the view hierarchy but it's defining it in, as a function of some other view. And that's what this thing is representing. You know it has a body method just like the view has a body property. But in this case the body method is a function of another view. That's what the content parameter is. And so what we can do here is apply this to any other view and apply some changes. And so in our case we want the transition-- we have the two transition states. We'll give it a single Boolean property saying is the transition active. And when the transition's active we'll, you know, apply two existing modifiers to kind of change the incoming view to take the effect of the transition. That is, we'll, you know, we'll scale it down and we'll fade it out when the thing is set.
Okay so that's our transition mostly defined. We have one final step though. We have the view modifier now but we need to give the system two values of the view modifier, both the active and the inactive values, so that SwiftUI can kind of package that up as a single transition and then as things are added or removed, pick the right value to apply at the right time and then interpolate between them during animations.
So that's the thing we can go on and use. So having done all that we can now see what we've built as a demo.
Okay, so here's the app you can download, and I'll run it. And hopefully-- yeah so we've got our window here. It's empty right now because the data model is empty but I can add things. You can see they take on the transition we just created. They're kind of scaling up and filling in. The nice thing is, you know, we can tap on them and remove the items. As you see as I remove something from the middle it'll kind of rotate around nicely because of the way we defined the shapes. And then I can turn on the kind of background animation here.
And then, you know, we have kind of a physics simulation running on this. And it's just effectively kind of random walking through the parameter space. And this is, you know, it's a nice little app. And even while it's animating it's obviously still all interactive and, you know, testing, it is working correctly. The interesting thing here though is when you think about how we're rendering this, SwiftUI effectively creates a native platform view such as a UI viewer NSView for every element that appears on the screen. And so that's the things like the buttons.
And so typically that's exactly what we want because, you know, we are mostly dealing with normal controls and things like that.
But for things like this kind of graphic display, you know this is, if you were drawing this in UIkit or AppKit this is probably not what you would do. You probably would not create a single NSView for every one of these things. And the reason being that once you start creating lots of them, you know, the performance isn't quite what you'd like. And that's really not a problem because, you know, we're not supposed to use NSView in this way.
So we have a solution for that in SwiftUI and that is we can effectively put everything in the ZStack here-- whoops-- inside something called a drawing group.
And a drawing group is a special way of rendering but only for things like graphics. So you know shapes and text and images, things like that. And when we do that we'll actually flatten all of the SwiftUI views into a single NSViewer UI view ad render them with metal. And so when I start doing this, you can see it acts exactly the same because, you know, it's not a behavioral change. But once I start ramping up the numbers of elements, hopefully you can see that the performance was a lot better because-- And that's simply because you know now there is one view which is kind of what the view system wants but you know the drawing's happening using hardware acceleration once only. So I think that's about it. Cool. Okay.
Okay so we've seen a few of the graphic modifiers in this talk but I want to point out that there really are many more. We've done a lot of work to basically implement everything you'd find in a normal 2D drawing system. And as we said, they all apply to views as well. So, you know, if this is, if you need something from here, just go look in the documentation. And I think this really kind of brings home the power of the model we've built. And the-- you know we want, we want to kind of use these graphic things but in cooperation with all the rest of the APIs like layout and animations and interaction. And the whole point of SwiftUI is that we've unified all of these areas around the one view protocol to kind of give you everything in the same package. We think this is going to be incredibly powerful and we really can't wait to see where you take it in the future. So thank you very much. We do have one more lab today. It's, I think in an hour, so if you have any questions about this or anything else, please come along and find us. Thanks. [ Applause ]
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.