(upbeat music) - Stanford University. - Welcome to lecture two of Stanford CS193p, Spring of 2021. In lecture one, we
started off on our quest to build a card game here
with just a little text, "Hello, world!" We learned how to modify
it, change it color, put padding around it,
and then how to combine it with another thing, this
RoundedRectangle using a ZStack. Now we're taking an even bigger jump, which is we're going to make multiple of these cards, making multiple cards. We're not going to quite jump all the way to having a whole grid of them here. We're going to start by just
making a single row of them, it's actually quite easy. We just need another View combiner. This View combiner is called an HStack, that's horizontal stack or
stacking them horizontally. I'm going to take this ZStack
that we built right here, and I'm just going to
put four of them in here. All I used was an HStack
to combine these ZStacks. This code is kind of gross though, if you really had to program this way. Imagine we have 20 cards.
We're going to have to have 20 of these ZStacks around? No. I mean clearly, we're not
going to do it this way. And why? Because, you know, for
example, this still says, "Hello, world!" We want
these to be emoji in here. So we're going to what, edit each one of these to be something, no,
we're not going to do that. It makes no sense. How are we going to clean
this code up dramatically, to make it make a lot
more sensible to a reader and to make our code really manageable? What we're going to do is
create a dining room chair Lego. Remember I told you
that we can create Lego out of other Legos. We kinda did that with a ZStack here. We created a one card Lego, kind of a dining room chair Lego. We kind of did it on the fly. There's no reason we can't
create a new struct that behaves like a View, which is a CardView. That dining room chair View
if you want to think of it in the Lego terms, we can
do that anytime we want. And in fact, we're going to
do that all the time because in SwiftUI, we really want
our Views to be quite small. We want them to have
very little code in them. It's better to have more smaller Views, than fewer larger Views. A View with this many lines
of code, that's pretty rare. We're only just getting started here because imagine how complicated
our card UI is over here. And yet I'm telling you
that even this many lines of code is a lot in a View. Lots of small Views, that is
the design paradigm of SwiftUI. So we're just going to
take this little ZStack of the RoundedRectangle and the Text, and make our own new struct
that behaves like a View. I'm going to call it a CardView. Literally just kind of go
down here and type it in. It's a struct. It's called
CardView. Tt behaves like a View, that means
it has to have a var body which is some View. I'm
going to set the value of it by throwing a function right there. It's going to have to return something. It's going to return
this ZStack right here. So I'm going to copy and
paste that right there. Of course, we don't need the
return statement right there. And voila, I've now created
my dining room chair View, which is my CardView right here. And I can use that, up in here, in the same way that I
would use any other View, any other Lego, like
Text or RoundedRectangle. I can just replace all of these by just putting a CardView here. We know that open parenthesis
and closed parenthesis is how we create something. So put four of those in there. Now, if I resume, I get
the exact same result as when I had all those ZStacks in there and look how simple all
this code has become. Much more understandable,
much simpler to type in, and much more powerful
all at the same time. This, what you see right here. This is the heart of
doing SwiftUI programming, is defining these dining room chairs and then stacking them
together and modifying Views as you need them along the way. Let's go ahead and make our
things look a little more like cards by getting
rid of "Hello, world!" and putting an emoji there.
Pretty much all Mac apps in their edit menu, and
Xcode has got the same thing, have this emoji and symbols. You can also do Control-Command-Space
to bring that up. We're going to do the same
thing that we did over here and have travel ones. See our little travel emoji right there. You can go back here and
we'll start with airplane. I've just put an airplane in
there and that worked great. It's kind of small little airplane. These are huge cars, but small airplane. So I'd like to make this a
little larger. Again at any time, even after I've done this factoring, I can always click on this text, go over to the inspector. For
example, it's font can be set. You see Font Inherited. I'm going to just switch
it to Large Title. I believe that's the largest
built in font that there is, and that made it larger. It's still not quite as large as these ones are over here. They're still kinda small and
we're going to learn next week a little bit about how to
make the font even larger, and also to make the fonts fit the size of the card they're on. So this is a smaller font
and this flags for example is for a much larger font. We'll learn all about that next week, but for now, we're going
to use this built-in font that have here Large Title. It makes it look a little bit better. We're pretty proud of
ourselves having gotten this far, building our card
game but it actually has kind of a fatal flaw that's a
little bit hard to see. The reason it's hard to see
is that we've been doing this whole thing with our preview window in what's called light mode. In iOS 13, they introduced this concept of having your phone in
dark mode or light mode. In dark mode, the
background is black, pixels are basically off as much as possible. And your UI is light colored in contrast to the black background. And these cards look fine. They look just like what we see over here. And you think, oh yeah, we did a good job. They're kind of tall and skinny but they at least look like cards, but they don't look
very good in dark mode. And it's a great opportunity
for me to show you how we can adjust this preview, because we just accepted
what this preview has chosen to show us. For example,
this happens to look like an iPhone 12 mini because
we ran that in the simulator. It chose that for us.
Maybe when you run it, if you don't choose iPhone 12 mini, it'd be something different. Well, I want to be able
to control what kind of phone I see right here. And maybe I even want to
have different kinds of them, looking here at the same
time as I change things. Here's how we customize this preview. And I'm not going to go into great detail, but I'm going to give
you the general concept. And just like with all of these things you're going to learn more
about it as the quarter goes by. So this preview is
controlled with this code that we moved out of the
way. It's this code down here that controls how your preview looks. Right now, don't worry too much about these two lines of code here, but you can see it's just
displaying our ContentView. If we click on this ContentView, it has an inspector as well. We can, for example, choose a device. We can also pick the color scheme here. When I pick dark mode, it
puts this into dark mode and you can see our cards don't
look so good in dark mode. It added this line of code right here. This is a ViewModifier. It's modifying our ContentView.
Remember our ContentView is just a struct that behaves like a View. So, it can have whatever ViewModifiers, padding, etc. So this is just like padding. It's a ViewModifier that
says, "hey, when you display this thing, please prefer
this color scheme dark." So now I see dark. But
what if I want to see it in dark and light? I want two simulators in here. No problem, we can actually
copy and paste this, and have the second one
instead of dark be light. Now I have both the
light and a dark preview at the same time. I can just scroll back
and forth between them. Inside here, we can put
however many Views we want. This is essentially a
bag of Lego View builder and we just list as many as we want, and it'll make a simulator
for each one that encounters. What's wrong with our cards here? Well, they really want to
have this white background even in dark mode. It doesn't really make sense for the cards to essentially be transparent,
where we can see through. We want this filled in with white. How are we going to fill
this thing in with white? Unfortunately,
RoundedRectangle, there's no way to have one be stroked
and have this red border and then also filled. So I haven't got fill here, actually it would mean something
slightly different. When we said stroke, that actually took our RoundedRectangle,
kind of made a racetrack around the edge of it. That racetrack is only 3 points wide but it could be even wider. So when we say .fill
right after a .stroke, we're actually talking
about filling the racetrack that we made around the edge. In Swift, you can fill with
other things beside colors. You can fill gradients and
patterns and things like that. So you could see how fill
could be useful for filling what's actually in that racetrack. But here what we want is
to fill the inside part, the part inside the stroke,
not the racetrack itself, but what's inside the racetrack, the middle of the RoundedRectangle. We can't do that with a single rectangle. There's just no way to
both create the racetrack around it and build the middle of it. So we're going to need two
RoundedRectangles actually. I'm going to copy and paste this, and I'm going to create one
RoundedRectangle that's filled. Then on this Zstack,
remember ZStack stacks them on top of each other. So I've got a filled RoundedRectangle, and then I put the
stroked one on top of it. Of course, I want the
filled RoundedRectangle not to be filled with red. I want it to be filled with white. So I'm going to say down
here .foregroundColor(.white) That's going to fill our
RoundedRectangle with white in the background there, and
then put the red on top of it. It looks good in both
dark mode and light mode. That was a little bit
of an excursion mostly to talk about preview, a
good example of showing why I would want to go down to my previews and create other previews. We'll learn as the quarter goes by that there are other
ViewModifiers that you can do that make them appear differently when they're being previewed. All right, so that's enough of that. We've got our nice cards here;
we've got multiple cards. That's good. What about face up and face down? All of our cards here are face
up, but in our UI over here, these cards are face
down, and if I touch them, they go face up. Face up, face down, how do I make my card appear face down and then how do I make when I tap on it, it'll switch between
face up and face down? Both of these things are
pretty straightforward. Let's go and talk about face down. Here's our code that looks face up. I don't know if you
remember when I first talked about these Lego bags, the bag of the Lego
things that we're passing as the content argument here
to the ZStack View combiner. But I said it could have ifs. I can put an if in here, so if isFaceUp, which I just made that up
by the way, this isFaceUp, we'll have to do something
to make that exist. Then I'm going to do this. But if it's not face up,
then I'm going to just have a filled RoundedRectangle, because that's what our
face down cards look like. This isFaceUp, I just
pulled it out of my hat, so it's not really something. That's why it says it cannot
find isFaceUp in scope, but all you need to do to
have something like isFaceUp that controls how I look is
just add another variable. So I'm going to say var
isFaceUp is of type Bool. So it is a Boolean. And that's going to get rid of this error, because I've got this
Boolean and I'm just checking this Boolean in my struct,
couldn't be simpler. Uh oh though, got some errors up here. Why did fixing this isFaceUp thing here cause errors up here where
I'm creating my CardViews? Well, this is actually happening because of a very important rule in
Swift and the Swift language. So let this sink in, this rule, you cannot have variables
that have no value. Variables always must have a value, from the time they're created all the way for their entire lifetime,
they must have a value. There's no such thing as a
variable with no value in Swift. There is a kind of
variable called an optional which we'll talk about next week. That looks like it's no
value, but it's not that it's no value, it just
has the value not set yet. It's a little bit
different, but a Boolean has to either be true or false at all times. This up here and, I'll explain
why this error's happening up here in a moment. But let's talk about how
this thing can get a value. This is just a variable so
we could do the same thing we do here. Let's just put a little
function here and have it return false. isFaceUp, the value of this variable is now going to be calculated by executing this function. This function returns false. So when we go back here
and try again to build- by the way, when you make a
change that won't compile, sometimes your preview will
say, "Ah, I can't do this. It doesn't work." When you try again, it doesn't always work the first time, sometimes it
takes a couple of try against, but it does work, look at that. All of our cards are face down
or isFaceUp returns false. They're all faced down. And if I change this to be true, then all of our cards are face up. So that's one way to do it. But we would probably
not use a function here when we can do something much
simpler, which is equals true. When we say equals true,
we are giving this variable an initial value. Currently with the way our code is, it keeps that value forever. So these cards are always face up, or I can say false and now resume, and they're all going to be face down. So that's one way to do it. But what if I don't provide a value here? Don't do the function way and I don't do equals
something, then what happens? Then I get these errors up here. What this is saying is
"Missing argument for parameter 'isFaceUp' in call" to create this. If you have a variable
in one of your Views, and it's not given any initial value, then you have to provide that value when you create the View,
and we do that by saying isFaceUp: true, for example. Notice that eliminated that error for this line, completely gone because we are now initializing this variable. This hopefully to you
looks very similar to this. When I was creating a RoundedRectangle I had to provide this
argument cornerRadius. This is not the only
way to make this work. So I'm not sure if cornerRadius really is a var inside the RoundedRectangle View, might be though. But if
it were, this is exactly the same kind of syntax
that you're going to get as we get up here. We can do this for all our cards, giving them all a isFaceUp, and we'll maybe have them be different. Let's have maybe the two
middle ones be face down, and the two sides be face up. We resume. Sure enough, it's working perfectly. This is an important thing to understand about these variables,
they have to have values. And if you don't set them
inside of your own View, then whoever creates them
is going to be forced to do it. By the way, if you define
this to be, for example, true, then this is going to override that. The person who's creating
it wins, if you've got both a default value and you
specify a value here. So notice that these two
guys who say that it's false, they're still winning.
Those are face down, even though the default is
true. But it does allow us to remove this. You can remove
this, perfectly legal now because there's a default value. And if we remove it from the two, then we're going to have
them all be face up. Before we move on with our card here, I want to take a little time out here to show you something else we can do with a bag of Legos View down here. When we added this isFaceUp var, kind of copied and pasted a
little bit of code right here to make the back of the card. We had this. We copied and pasted it. I'm just starting to notice
that that copying and pasting is resulting in my having this same expression
multiple times in here. And of course in programming,
we don't want to do that. We want to be able to use
local variables, for example, to store things like that. A bag of Legos View, in
addition to having a list of Views obviously and
these if-thens, can also have local variables. I'm going to create a local variable here, I'm going to call it shape,
which is going to be the shape of my card right here, shape equals. I'm just going to bump up
my RoundedRectangle here. And now that I have this local variable, I can replace all the times
that I use this with shape. Shape here, shape here, shape here. Since this has made this
such a small little word in my line of code, I
probably would even put these on the same line right here. I could do the same thing with my Text. So I've got a double benefit
here of not repeating typing this over and over, and making
my code a little tighter, a little easier to read, a
little easier to understand that there are three
things that are ZStacked together here to make that work. You notice that doing this introduced this little warning here,
this yellow warning. We always want to fix our warnings. The red errors we have to fix, but the yellow warnings,
we have to fix those too. Our program might function,
but a warning sometimes is an indication of
something not going so right. Let's take a look at this one. It says "Variable 'shape'", That's this thing I just
introduced, was never mutated. It says, "Consider
changing to 'let' constant" So it's saying call
this let instead of var. So I'm going to do that, instead of var I'm going to type let. That means let this shape equal that, and no more warning. What is the difference
between var and let? Well, var is short for variable. That means that something
that's going to vary. something that's going to change. Clearly our RoundedRectangle
is going to always be our RoundedRectangle here,
it's not going to change. So that makes this really a constant. We always want to use let,
when we're defining variables that are not really
variable, they're constant, and use var when they're variables. This is a var because
it's changing every time someone asks for it, because
it's executing this function to find the value. So lets are just constants
that we set things to. One thing that's
interesting about this line of code here, kind of different
from this variable up here, is that there's no type,
right up here we had the type Boolean. Here, we
have a type also, some View. There's no type in here, and
of course the type of this is RoundedRectangle, and we
didn't have to type this. Swift again, doesn't
like us to have to type the same thing over and over. So it says, "Oh, no problem, I will figure this out for you. You just leave this off. I
will figure it out for you." That's again, not only
good for less typing but what if we decided,
we left that in there, we decided, no, I want my
cards actually to be circles. Let me type a different kind
of shape in here, a Circle. Well, of course, that's
going to generate an error because a Circle is not
of type RoundedRectangle. But if I had left that off,
then when I changed this to a Circle, it would all just work. Look, those things change
to be circles instead of RoundedRectangles. And this shape automatically
changed to be the right type. We'd let Swift infer the type.
You're going to hear me use the word infer, when I'm
talking about types a lot in this course. And inferring a type just means that Swift are going to look at the
context that this thing is in, if it can, and most of the time it can, it'll put the type in there for you. So, we do not put these
exquisite types in there, when Swift is able to infer them. Let's go back to RoundedRectangle. Next thing I want to do is,
have it so that we can see our face up in real time and
live action with the user, by making it so that when we
tap on one of these cards, it flips it over. Just like we have in our
app over here, every time we click on it, again it's
not going to be animated like we have over here, because
we're just getting started, but at least we'll be
able to see the face down and the face up versions
of our card really easily. How are we going to do that? Very simple, I told you that
something that behaves like a View, like our CardView does or even like our
ContentView does up there, it not only knows how to
draw in its rectangle, but it knows how to
receive multi-touch events and react to them. We're
actually going to spend pretty much an entire week
talking about doing that. But the simplest way is a tap gesture. You can do that with a ViewModifier. So I'm going to add a
ViewModifier to the Zstack. My ZStack represents an entire card. It's called onTapGesture. It takes one argument which has got the perform label here. And the argument is a function. Now again, surely you're not surprised to see functions as
arguments at this point, but this function, it's not
a ViewBuilder or anything, we're not making a list
of Views or Lego bag here. Here we're talking about
just a regular function where we can do anything we want. And in fact, this function
does not even return anything. It just lets you do any
code that you want in here. And since this is the last
argument and to this function it's the only argument to it. We can remove this little
key word and remove the parentheses, so this looks really nice and clean as well. What do we want to do
when we tap on a card? Well, we want whatever
isFaceUp-edness it has to flip over, to be the opposite. So we essentially want to say, isFaceUp equals not isFaceUp. We just want it to flip over.
But unfortunately you can see I'm getting errors here: "Cannot assign to property:
'self' is immutable" It's basically saying I can't
set this variable right here, because self, self means my
entire View, that's the code I'm in here, is immutable. And in fact that is the
case. All Views in SwiftUI, every single one you see here, every one you're ever going to to see, is immutable, cannot be modified. And you might say, well, how
does my UI ever change then? Well, when things happen, for
example in your game logic, your entire UI is being
constantly rebuilt. New Views are being created
to reflect the changes in your game logic and
replacing old Views. And of course that's
happening very efficiently. That's one of the great
things about SwiftUI is that it allows for this entire rebuilding of your UI, highly
efficiently, but all the Views are being replaced. There
is no way to actually change this var even though we call it a var. Once it's initialized either
by the creator up here, or by using this default
value, it cannot be changed. That takes a little bit
of getting your head around to understand that
when our UI is changing, it's because all these
Views are being rebuilt but that is the case. Now we are not going to
be working on the logic for our app until next week. And it is the logic that
drives what the Views are supposed to look like. But, there is a small way
that you can kind of create your own, can think of
it as like, mini-logic but it's really just creating
a tiny little outside of the View storage space, and have the View just look at that. And when that changes,
the View will get rebuilt but still pointing to that
little piece of memory. And you do that by changing one of your vars to be @State. So putting @State in front of a var, a lot of people when they
first started learning SwiftUI, they think, "Oh, that makes this writeable somehow, that
now this View is mutable. It's got this variable to be
writable." But that's not true. This View is still immutable. It just turns this variable
instead of really being a Boolean, it's actually a pointer to some Boolean somewhere
else, somewhere in memory. And that's where the value can change. But this pointer does not
change. It's always pointing to that same little space over there. Now that might seem
like a fine distinction, and it is somewhat of a fine distinction but it's not really too
much to worry about because we really don't use @State very much. It's pretty good, at the
beginning of the quarter when we don't have our
game logic to drive our UI, and we just want to do
something like change our isFaceUp around. But when we actually start building apps, we're not going to use @State very much. It's mostly just for temporary state. Things that are like, I'm
in the middle of a drag or a pinch multi-touch, and
I want to keep some state as the drag's going on, and
then when the drag's over, then I don't care about the state anymore because my logic will
have changed presumably. Or you can use it just for
state that only affects the way your View is displayed. Not the logic of how your UI
is playing a game or whatever. So we'll use this little
@State. Notice that the arrow went away. Now
we are able to do this isFaceUp. If I click on
cards, okay we've failed here of course because we introduced
an error, so let's rebuild. All right, there we go. Now if I click on a card here, hopefully it should flip over. And click. Oh, wait a second. My preview here, when I
click it selects things. How am I going to check to see if my onTapGesture works? Well, one way is I can run my simulator, so I'm going to go launch my
iPhone 12 mini simulator here. Here are my cards, and I tap. Woo hoo! It's started flipping them over. Nice! isFaceUp, isFaceUp, up and down. Very nice, so that's
working and this works here. We can rotate, works over here, very nice. So that's good. It's a little bit of a bummer though, that anytime I want to test
some multi-touch gestures, I have to go all the way
back out to my simulator. I've been so used to
doing it here in preview. Well, of course we can do it in preview. All we need to do is use this
little button right here. You see where it says 'Live Preview' If I click 'Live Preview,' then it goes out of
the mode where clicking on things selects things, and I still have pause and resume, so
I'm going to resume here and it's going to be
showing me the latest UI, but now tapping in here
is not going to select, it's going to do any multi-touch gestures like this one. You can switch back and
forth really easily. Turn off live preview,
you're back into clicking on things to select them,
then back into live preview to make sure that your
multi-touch gestures are working. Just to summarize here, when
we are changing isFaceUp as a result of this
onTapGesture on the ZStack, we are actually changing
it in memory somewhere. And our isFaceUp is pointing to that. So whenever it changes, SwiftUI
is smart enough to notice oh, whoops, this thing is pointing at some isFaceUp Boolean somewhere that changed. I need to rebuild this View and it will recreate the body right here. And when it recreates the
body, of course the isFaceUp will be different. It'll choose whichever of these make sense, whichever of these if cases make sense. That is the process by
which this is happening. And this is a little miniature
of the way SwiftUI works once we really have our game logic going. When the game logic changes, it notices which Views are
going to change as a result, and it rebuilds their body. So that's why it's
rebuilding this all the time. These things are read only. So if something changes we have to rebuild this View, rebuild its body. What else can we do here? Well, all of our cards,
unfortunately, our airplane cards it'd be really nice if we
had our cards be something like this, where they
were different vehicles than airplanes, so let's do that. Where is our airplane? Okay, airplane is right here. This little String is
in every single CardView we ever create, that's
why they're all airplanes, but what if we parameterize this? Just like we have this isFaceUp var here, what if we create another
var, we'll call it content. because it's the content of our cards. It's just going to be a String. A String, by the way, very
important struct inside of Swift. It is obviously used
in Text and everything else. This is the kind of struct that you're going to
want to become familiar with by looking at the documentation. In fact, let's take a little brief look at how we can look in the documentation, because there's a quick way to get into the documentation from your code, which is to use the Option
key. Hold down Option and you see it turn into a
question mark there and click. When you Option-click,
it's going to give you a little summary from the
documentation about this type you clicked on, in this case String. It tells you kind of a little bit about how it works, but
you can also click open in developer documentation. It'll bring up the documentation. Now you get a long description of everything that String can do, I told you it was an important struct. It can do a lot of things, and a list of every
single function and var that it has, and what they
do, what the arguments are, how to call it, etc.,
lots and lots and lots and lots of stuff. In here, you can also search. If you go up to "Search
Documentation" up here, search for other things, and I try to make it so that you are going through this stuff as you learn the stuff.
Asking you to go through the entire documentation and learn stuff would be a challenge to say the least. SwiftUI is obviously very expansive, but this is something you
don't want to skimp on either. When you get to important
classes like String, and we're going to talk
about Array in a moment here, those are things where
it's really good to read this introduction section right here. I passed that. You're going to be
searching through here a lot to find functions and variables
that you're looking for. But eventually you will become a master of all of this stuff in the documentation. It can also bring up the
documentation by the way up here, in the Xcode menus window,
developer documentation brings that same window. If we have this content String, then we can just use
this content right here. Content, and that would
choose that content, whatever this var is,
instead of the airplane. Now just like with isFaceUp, this has to have a value. This can't be just set to
nothing. There's really no good default value
here. We don't really want our cards to default to being airplanes. Well, this is the same
errors we were seeing before with isFaceUp, and we're
going to fix it in exactly the same way by saying, all right, you've got this unset variable content, I'm going to set it for you. So let's set it and
pick some vehicles here. Edit -> Emoji & Symbols I'll take train. Train is cool. Do a little bit of copy and
paste to make this go quicker. We have now changed this content argument for each of our CardViews. And so when we resume,
our cards look different. All four cards are different. And we're still in live
preview, even though we resumed and we changed code so we can still click. That's really neat. We
got different cards going on right here, but it's a little bit, we would probably never
write our code like this where these four emoji
are hard wired in here. We really would rather
put these in an Array or something like that, and
then pick these out of an Array and eventually we'd like
to have a for loop in here. But I think I mentioned earlier
that these bags of Lego, while they can have
local variables and ifs, they can not have a for statement. So we're going to need a
special bag of Lego making View that does for loops and we'll
see that in just a second. But before that, let's
just put these four things in an Array because we need to learn about Array anyway. Array
is a very important struct just like String in SwiftUI. I'm going to put this up here,
var, I'm going to call it emojis, and I want it to be
equal to an Array of things. And in Swift you can just
put open square bracket, close square bracket, and a
bunch of things listed in there. And you just created
an Array of something. So if I want to put these
four emoji in there, it's as simple as that. Now, once again, notice I didn't
specify the type down here. I let Swift figure it
out for me automatically. But if we wanted to type
this here, what type is it? It is an Array, but just like in Java, you can't have an Array
that's just an Array of unspecified typed things. Swift is very strongly typed and it wants to know what type. So in angle brackets, you put
what type is in the Array. So this would be the type
of our emojis var here. It's an Array of String. Arrays are so common to create an Swift that there's a little bit of
a shorthand for doing this, which is [String]. That's just another way
of saying Array of String. Exactly identical, not a different type. It's exactly the same as Array<String>. But again, we don't need
that. Let's take that off. We'll let Swift infer it
from what it sees here, which is that it's an
Array with Strings inside. Now that we have this Array
right here, we could replace these here with emojis[0], for example, and then emojis[1]. So Arrays are indexed from zeroth element
like most languages do. We have to be very careful here. We wouldn't want to say
emojis[4], oops, or emojis[5]. We want to stay within the bounds of it. Because if you access an
Array outside of its bound, if you use an index here
that's outside of its bound, you will crash your app. Your
app will raise an exception and crash. Kind of, you want
that because if you were outside of the bounds here,
that's probably not good. You'd want to find that in
your development process before you ship it out to your customers. That's pretty cool. We got our
emojis there. Let's make sure that still works. Resume. There it is, works nicely. By the way, that Option-click
that we did down here on a type, if you Option-click
on a variable like emojis, it will tell you the type. In this case it says that
it's an Array of Strings. And of course you can
click here and go directly to the type documentation as well. We're getting closer and
closer to making this kind of a more extensible, expandable
kind of UI for our cards. The next thing that I
want to do here is do that for loop-like thing that I
was talking about in here. We can't use a for loop,
but we'll talk about what a for loop looks like in Swift later. But first we're just going to
do this new bag of Lego thing, that essentially is a bag
of Lego that creates a View for everything in an Array. That'd be exactly what we
want here, we want to create the CardView for each of
the things in our Array. This bag of Lego View that
does that is called a ForEach. And it just takes the Array that you want to create
something for each. And then it has a content parameter which is another bag of Lego. So it's going to
essentially create this bag of Lego this many times,
however many things are in here. And then this itself will
become a bag of Lego. So since this is going
to be a bag of Lego, lots of Lego here, it only
makes sense to put a ForEach, inside something like an
Hstack, a View combiner, because the ForEach itself
is not a View combiner, it's just a bag of Lego maker. It makes a bag of Lego filled with stuff, by doing something for each
of the things in this Array. This content can just be our CardView. So for each of the emojis in here, we're going to build a CardView, so now we don't need these. Of course, we're not going
to build the CardView with the first thing in the Array. We really want to build
it for each emoji somehow. Somehow we're going to have
to specify the emoji each time this ForEach goes through. And to do that, we're going
to show you something new, which is that functions like this which is a special ViewBuilder function, but it's still a function,
can have arguments. None of the functions we've seen so far, that we've used here,
has had any arguments. Some of them have
returned things obviously, but none of them have had arguments. So how do you put an
argument to a function? Well, you just put the name
of the argument and in. And there can be multiple arguments. Of course, arg2, arg3, you can have as many arguments as you want. In this case, the ForEach only
wants to pass one argument, which is the thing that
it's currently working on as it goes through each of these. So this is going to be emoji
Strings, so it's going to be the train the first time through,
and we're going to create a CardView of a train. Then there's going to be a
rocket ship the next time through and we're going to create
a CardView for the rocket. And those are all going
to be put in this Lego bag that's being created by
this ForEach, very simple. We still have an error here, it says "Referencing initializer
on 'ForEach' requires that 'String'" which is
the things in the Array, right there, "conform to 'Identifiable'" You're going to see this
phrase 'conform to' quite a bit in this class, and it's really the same as what I was saying when
I said, behaves like. So this ContentView behaves like a View. This CardView behaves like a View. These things in this Array,
if you want to do a ForEach, they must behave like an Identifiable. See this thing, Identifiable.
What is an Identifiable? Any kind of struct can be an Identifiable, but it has
to have a var called id, which uniquely identifies it. Why does the ForEach want
the things in this Array to be uniquely Identifiable? Because it's going to create
a View for each of them. And if, for example this Array right here should be reordered, or
new things added to it or things removed from it, etc., it needs to know which
things changed in the Array, and then adjust the Views accordingly. So it needs to be able to
uniquely identify everything in the Array to match up the View that it's going to create for them. So it makes total sense. Of course, the things
that we're doing ForEach, we have to be able to
uniquely identify them. Now, unfortunately, a
String has no such thing, If I had two trains here,
there's no way to tell this train from this. They
look exactly the same. This is not going to be
a problem when we build a real card game over here,
because our ForEach is not going to be going ForEach through emojis, it's going to be going
ForEach through cards. Our game obviously knows
about some concept of a card, and we have to go through
the cards because some cards can be the same. This checkered flag and this one, same emoji, but those are clearly different cards. Look, the timer is different
on this one right here. And obviously I have to see if
you've chosen these two cards and see if they match. So they have to be different. So it's not going to be a
problem when we have the logic of our game, but in our
little demo here where we're just making Strings, it's a problem because there's no way to
uniquely identify the Strings. So we're going to work around
that in our demo world here, just by picking a different
String for every single card. All these cards, look at
that, different vehicle. There's no two vehicles alike and we're just going to
have to live with that. And if we do that, then we
can go back here to ForEach and tell the ForEach, "Yeah,
just use the String itself as the unique identifier, even though it's not actually
unique, do it anyway." We do that with another
argument to ForEach, in addition to the emojis and
the content here called id, which tells it how to identify it. And it's kind of weird syntax here, which is we do \. and
then we can put the name of a var here that String responds to. And that's what it's going to use as the identifier. String
doesn't really have a var that's unique as we know. So we're going to use a
special var called self. All structures have this var
on themselves called self, which means the struct itself. So that's perfect for String. If I say, .self on a String, I
get the String itself. This String is in fact, the identifier that the emoji is going
to use to identify them. Let's see what happens when we do that. We'll run it in our simulator first, because we want to be able
to put these over here. There we go, we got our five cards. See, two trains and
the other three things. That's what we have here. Two trains and the other three emojis. And if I click them over, they're working great, working good. Oh oh, you see when I tap on the train, it's using the same
View for both of these, because, again, the ForEach,
can't tell these apart. So when it sees the train,
it uses the CardView it's already made for a train over there. That's kind of weird behavior and you really would just
never want to do this. You don't really want to have Arrays that have non-Identifiable things in them that you pass to ForEach. So we're not going to do that. We're going to go back
to having just the four. We're still in preview mode over here. This is working fine over here as well. We can clean up our code a little bit here because of course, content
is a function, an argument that's a function and it's the last one. So we can take that off of there. Get rid of the parentheses. The next thing we're going to do is put these little buttons on here: add cards, remove cards. We want be able to add or remove cards. To add and remove cards, we
can't have this fixed Array that is always being entirely
displayed right here. We want to be able to vary the number of things that we're showing. So I'm actually going to do that by creating a much larger Array. And then here only showing
a subsection of the Array depending on how many of
these cards we want to show. And just to make this typing go fast, I actually have a little code
snippet that does 24 vehicles, but I'm only going to show the first, let's say, six of them. So how do I have an Array,
a big Array like this and then only show some of them? We can index into the Array. And instead of doing a
single index like [5], which should just be one
of them, I can actually do what's called a range. This syntax right here is a range. There's both this kind of
range which is zero up-to but not including six, or up-to
and including the number six which would be seven items over. So here we're saying, just
pick the first six of these. So let's see if that works. Sure enough we got six. We can flip them over, looking good. We should be able to do
the same thing right here. Resume, get six, excellent. If we change the six to four now we're only getting
four. We change it to eight. Now we're getting eight. Our plus minus is essentially changing this number right here. When we go over here and
say more cards please, or fewer cards please, we
are changing that number, that four, so this just
wants to be a variable. So let's make a variable for that. I'll call it my emojiCount,
and it's going to be an Int. We'll start it off at six, let's say. Of course we can have
Swift infer that Int. We don't need that,
because this is clearly an integer right here. If we said 6.0 by the way, we'd infer it to be a Double, precision
floating point number. So let's take this emojiCount and make that be the max, emojiCount. Then let's resume over here. We got six. If we change
this emojiCount to three, we'll go down to three cards. If we change it to ten then we have ten cards. So this is great. All the
buttons we are going to add here have to do is change this emojiCount var. How do we add a button
to our user interface? Make it a little cleaner here, How are we going to add a button? We want a button somewhere in our UI which is add a card and remove a card. Well, adding buttons is
really easy. Of course there is a Lego brick, which is a Button. We're going to put it
right below our Hstack. So we've got our HStack right here. I'm going to put this
Button right below it here that adds a card. And how do I stack this HStack
on top of something else? I'm going to use another
View combiner, a VStack. VStack stacks this stack with a Button. And what are the arguments
to making a Button? It has two arguments basically, the action which is a function, and the label which is also a function. It makes sense that the action
would be a function similar to onTapGesture, had
a function right here. But what about label? Why is the label of a Button a function? Well, the label is what
appears when the user sees on screen to tap on. And usually this would be, for example, a Text, like our Texts that might say, "Add Card" or something like that. And you can see it's actually
appearing down here in the UI. It's a function because
it's a ViewBuilder. So it's a bag of Lego. You might be making more complicated types of
content for your cards. For example, it might just
be a single Text like this. What if I wanted to be "Add"
with "Card" on the second line? Well, it's perfectly allowable
for me to make a VStack in here, with Text saying
"Add", and another Text saying "Card". Here's my label. Here is the function, the ViewBuilder function,
that's returning the View. So this can be an
arbitrarily complex View. Anything you want in
here is perfectly fine. So there's "Add Card", and maybe we have another Button
called "Remove Card", got "Add Card" and "Remove Card". And the action for each
is obviously different. An action for an add
card is emojiCount += 1. This is emojiCount up
here, and this Button is emojiCount -= 1. We've got our Buttons
here, kinda looks okay. But we have a problem,
same problem we had below with isFaceUp, "Left
side of mutating operator isn't mutable: 'self' is immutable" We know that these are immutable. Again, we'll just for demo purposes, put this off somewhere
else and have emojiCount. This should be a pointer to it. And that makes this work. And you might find in your
homework, now you might want this to end up being @State as well, A little hint hint there. This is nice but I really
don't want Add Card and Remove Card stacked
on top of each other. I want them one on each
side like this, minus and plus, the minus one on the left, and the plus one on the right. How do we do that? Again,
just throw them in an HStack that will stack these things horizontally. And if I want the second one here the Remove Card to be first, I can just pick it up and put it here. Now I've got my Remove Card and Add Card. Try again here. Yep, Remove Card and Add Card. Again, not quite what I want. I want them more spaced out. There is another Lego brick
that you're going to use that you'll use all the time
when laying out called Spacer. And a Spacer is always going to grab as much space as it can. The Remove Card and the Add
Card, they're going to be as small as they can
just to fit their Texts. And the Spacer will grab all the rest. But this has pushed these a
little too far out to the edges. So can you think of how I
might make these not go quite so far to the edges? How about if I take this HStack and add some padding horizontally. Move them in a little bit. Code is starting to get messy
again. This is pretty long. I told you these Views are supposed to be really, really short little Views. We could turn this Button
here into a new struct, but that's a little bit overkill. When we created this CardView struct, it made a lot of sense
because first of all, it was more complicated.
It had the ZStack with all this stuff in, whereas
these buttons just have a single Button. But second of all, it had all these vars and all these things to configure it, etc. And the Buttons really
don't have any of that. When you have something like this that's making the code really
long and hard to understand, but you don't want to
go so far as to make it its own dining room chair Lego. Another solution you can do
is to create a var for it. We could create a var, let's
call it remove. It's some View. It's going to be this Button. That's it. Yes, this is
saying return but since this is the only thing inside
this function right here that sets the value of this, we don't have to say return right there. Same thing we create a var
add for the add Button. It's some View as well, and just put this Button down in there. Now up here, we can say remove, and then the Spacer and then add. Now, this code is back
to looking really clean and very understandable. The last thing I want to do
is change this add and remove to these nice icons here,
these plus and minus icons. Where do those come from? If I was writing my own app
here, then maybe I would want some images for this, and
I'd hire my graphic designer to go and do them. But for standard things
like, give me more of these or give me less of
these, things like that, it's really better if you
use the same images that all other designers are
using out there in the world. It's because users get used to it. "Oh yeah, plus in the circle,
I know that." They click on it with confidence because
they've seen it in other apps. Apple has made hundreds and hundreds of these prebuilt little symbols. And the challenge for you is
just to pick the right one. And so they provided a really cool app for you to search around all
these symbols by keyword, and find the thing you want. Let's look at that
first, and then we'll see how we actually create
an imaged Lego brick that would show this. The app we want is called SF Symbols, and you get it by going
in your web browser, and going to developer.com/sf-symbols. And when you go there,
you're going to see this app. There it is, Symbols 2.1. You're just going to
download it and then run it. And when you run it, it looks like this. It's essentially just a big search engine for all of these images. And you can see lots and lots of them. They're grouped by type,
but you can also search. I might say, I want to add something. Now you're getting all the ones
that make sense for adding. And you can see here's
a plus with the circle. There's also plus with a square. I might be thinking which
ones I want to do here. But I think plus with the
circle is more appropriate to what I'm doing there. plus.circle is the name
of it, plus.circle. That's all you need to know is this name. And when you have this name,
you can put it in your code. So let's go back to our code
and add that. Instead of this Add Card VStack right here, two things piled on top of each other, we want a different kind of Lego brick which is called an Image. And we create the Image,
there's actually a lot of ways to create images. Remember back here in our
navigator we looked at Assets. If we drag JPEGs in here
and give them names, we can use our image by name there. But if we want one of those
SF Symbols from that app that we just saw, we
want to use systemName as the argument or the
label for the argument. And then you just do your
plus.circle that you found when you looked it up. That is just using a
different View. Resume here. We see we've got a plus right there. And we can do the same
thing for the minus. Let's go back to SF Symbols. Let's look for remove and
see what we find. Here we go. We're looking for a similar kind of thing. There it is, minus.circle it's called, So come back, minus.circle, Image(systemName: "minus.circle") Couple of things not quite
right here. It's getting close. First of all, these things
seem very, very small. We're already using Large
Title here for the Text on our buttons. We can do the exact same
thing and use Large Title for this HStack of these two Buttons. So just like the same
thing, I'm padding it here. I'm also going to say,
use the .font(.largeTitle) And that's going to filter
down inside this HStack into all of these Buttons. And they've also become larger. I can also put a Spacer perhaps between this HStack at the top and these Buttons down here below. I can even enforce some
minLength of that Spcaer. Maybe I want it at least 20
points or something like that. But a lot of times you want
standard default spacing because again, it can be device dependent, different on an Apple
Watch than it is on iOS. I know it's hard to
believe, but these things that we're building, we can build them so that they would work on Apple Watch. Or even if we're just building a very Apple Watch specific UI, we're using the same tools here. I think we've got this UI how we want it. Let's see if it's working,
so I'm going to hit plus. Yeah, five cards, six, seven. Oh yeah, lots and lots
of cards. Minus, oh yeah! One thing we weren't very
smart about here was that what happens if we go minus minus, minus, minus, minus, oh, preview crashed. Why? Because I kept going minus so much the minus count went below zero. And then I tried to
create this little range of stuff inside of my Array
where this was negative. So that's clearly no good. So we really need to protect ourselves against those Array index out of bounds. So here we'll say if emojiCount > 1, then we'll do emojiCount-1. And here we'll say if emojiCount < emojis.count, emoji.count this is just a var in Array that says how many things are in that Array. Now when we're over here and
we do minus, minus, minus, it won't let us go much
more minus than that. And plus, plus, plus, plus, plus, plus, it won't let us get to
more than 24 over here. The last thing I want to do is make it so this code is a little cleaner. You can see that this
Button has two arguments, both of them are functions. So you can do the thing where you get rid of these key words and do this outside, even if you have two at the end. And you just do it by putting the label of the second one, right
after the first one. Here I've got this one,
then just this label. And then this one. We keep
the label for the second one if we're using the last
two, but we still don't have any commas or space or
parentheses or any of that, get rid of the comma
in there and all that. We've got a little bit more to do, to turn this into a grid. But before we do that, I'm
going to take five minutes and just review everything
we've learned so far. We learned to build user
interfaces with SwiftUI, by building these data structures
that behaved like a View, and this idea of building
a structure that behaves like something, is the
fundamental underpinning of the functional programming
methodology in Swift. We learned that Swift also does object-oriented
programming methodology, and that we're going to
combine those two methodologies next week when we add
logic to our application. But at the UI portion is
all functional programming. We learned that a View is a
rectangular area on screen. It can draw. It can
receive multi-touch events. And we learned that
just saying that behave like a View, gives us enormous power. For example, we can set fonts in ourselves or set our padding or
even handle a tap gesture, just by calling a function on a View. But even more importantly,
we learned that we could put Views together to build
complicated user interfaces. And we get all that
mechanism just by saying we behave like a View. But, with that great power comes a little bit of responsibility,
which is that we have to implement this one variable in our data structure called body. And this body's type is some other View. So the body of our View
is some other View, and this might've been
initially perplexing, but it made sense once we started talking about Views like Legos, where we're going to piece them together to build more powerful Lego,
like a dining room chair Lego, and then piece that together
with other more powerful Legos to make a dining room
Lego, and then a house Lego and the neighborhood Lego, universe Lego. And once we had this
model, it all made sense, that the body of a given
View is some other View. Now the value of this
variable is calculated by executing this function,
which just gets dropped right in the middle of our code. And we found that dropping
functions in the middle of the code is something we do
all over the place in Swift. We did it a dozen times in building this, the user interface. It became commonplace for us to do it. This function obviously
has to return something that is a View, and we
know that the compiler is going to replace some View right here, with whatever the actual View
that returned from here is. This View that we're building in here, we make it somewhat out
of individual Lego bricks like Spacers and also Images, and also Texts down here. But largely we make them
out of these View combiners or Lego combiners as we called them. These Lego combiners, they are created like Lego bricks. They might
even have arguments, alignment or spacing or whatever, but
they all are going to also have this argument, which is
another function that's dropped in there, which is a list of
the Views for it to combine. And that list of Views we
call a bag of Lego View, and it's made with kind of
a special functional syntax called ViewBuilder. So these are ViewBuilders. They allow us to just list our Views. One, two, three Views
right here or down here we've got these three Views listed. We noted that we can also
do if then's to determine which Views get into our bag of Lego. And we can even do
local variables to clean up our code as well. Along the way, we learned yet another way of doing bag of Lego, which is ForEaches. ForEach creates a View for
each of the things in an Array. This Array, or in our case, a
little sub part of an Array, has to contain things
that are Identifiable so that the ForEach can
keep track of which things in the Array match which of
the Views it is creating. It normally does this
by requiring the things in the Array to behave
like an Identifiable. Just like we say our
structure behaves like a View, we could say that the things in this Array, behave
like an Identifiable. All it means to behave like
in an Identifiable is that you can identify it,
uniquely identify that thing in the Array from all
other things in the Array. We're putting Strings in
here, unfortunately, Strings do not behave like an
Identifiable because two Strings that look exactly the same
might be different Strings. So we kind of cheesed it
with this little argument right here, that forces the
ForEach to think that each of the Strings was itself Identifiable. And then we made sure to
never put the same String in our Array twice. So that was a bit of a cheese
when we do our actual app, we'll be putting cards in these Arrays, we'll be ForEach-ing
through cards, not emoji, and the cards will of course
have to have some sort of unique identifiability, and that's something we will
cover when we do our cards next week as part of our logic. We learned, of course,
that these View combiners, when we gave them a bag of
Lego that the Lego that was inside their bag of Lego
could be more View combiners that have their own bag of Lego. And those bag of Legos could have more bag of Legos inside of them. This allowed us to build pretty
complicated user interfaces, but our code started to
get a little bit long, and we know that the design
methodology of SwiftUI, is that we want our
Views to be really small, not a ton of code. So we started factoring
out some of the code. One way we did that is by
creating a dining room chair Lego. Creating our own struct
that behaved like a View, that represented a little
piece of UI in this case, one of these cards. And when we did that, we learned that these little dining room chair
Legos, could have variables that would configure how they look, whether it's face up or face down, what sort of emoji is on the card. And finally, we learned
that we could also factor out some code just by
creating our own little vars, that this body var up
here was not the only View that we could create. We could create our own
some Views, for example to add a button and put that inside, with just referring to the var, and this cleaned up our code quite a bit. And that's all there is. In the next week we're going to be adding
logic and in the weeks after we'll be adding
things like animation, but all of those things
are all going to be within the framework of
building UIs in this way. There's really not a lot more to it. There's a few more View
combiners for us to learn. There's a few more of these
Lego bricks for us to learn. And of course there's a lot ViewModifiers for us to learn. But for the most part,
you've already learned the fundamentals of
building Swift UI apps. Okay, the last thing
we want to do this week is put our cards into a grid. Instead of all, horizontally like this. We want them like this. We also want them to look more like cards not these tall, skinny things. Before we go off and do that though, I want to address one other issue here, which is these buttons down here. See, these ones are blue and
the ones over here are red. So why are these blue and these red? Well, this blue is kind of
standard control blue in the UI. And we really want these
buttons right here to be blue because when users see
this blue, they think, "Oh, I can probably touch on that." This red, pretty obvious,
but they might not be 100% confident that
they can click on it, and then we'll go do the grid. So let's jump back and do that. The red is actually
very easy to understand why these buttons are red, is because this foregroundColor(.red)
is being applied to the entire VStack of our
UI including the buttons. Fixing this so that only the cards are red is a simple matter, of
taking this foregroundColor out of here, and putting it on this HStack that has our cards, so
that it's only affecting this HStack. Sure enough, now the cards
are red and this has reverted to its proper default
standard control blue. Now that we fixed that, how do we make this
HStack here into a grid? This is very straightforward. We're just going to replace this HStack with a new View combiner
called a LazyVGrid. We'll talk about why it has
the word lazy in its name in a few minutes. A LazyVGrid basically lets you
specify a number of columns, and it's going to make
that many columns here, and then it'll make as
many rows as necessary. And, yes, there's a
LazyHGrid where you specify the rows here, the number of rows and it'll make as many
columns as necessary. So this is really great. Unfortunately, you can't specify
the number of columns just by putting the number
here, like three columns. Instead you put in an Array of GridItems. So however many GridItems you put here, there's one GridItem, there's two GridItems, here's three GridItems,
even more GridItems I put here, that's how
many columns it makes. Why does it do GridItems here, as opposed to just the number three,
that I want three columns? Well, because this
GridItems let you control the columns more. This LazyVGrid is of course more powerful than just give me three
columns and I'll do it for you. For example, I could put in
here GridItem(.fixed(...)) at 200 wide. And so now this first
column is fixed at 200 wide. The other ones are just fitting in whatever space is left over. And that's because the default in here is actually something called flexible. These are flexible GridItems. And so they'll flex to
whatever space is available. But, we're going to go back
to just normal three columns that are the same. And we're going to try and
understand what's going on with our UI here because
the cards have changed shape. They're kind of these short little guys. They're wide but they're short, and they don't really look
like cards much anymore. Although, they didn't
look much like cards when they were in HStack either. They were really tall, way
too tall for being cards. Why is the LazyVGrid doing this? Why is it making them this size? Well, the HStack, it uses
all the space it can, whatever space it can
get a hold of it uses it and it uses it in both
directions: height and width. That's why we got tall
cards that were as tall as they could be and as
wide as they would fit. So that is the HStack's way
that it distributes its space. A LazyVGrid has a different strategy. It uses all the width horizontally for its columns, depending on the fixed and flexible in here, it's using all that space. But
vertically it's going to make the cards as small as
possible so it can fit as many as possible. So that, for example, if we go
to emojiCount = 24 right here and resume, all 24 cards are
going to fit in this LazyVGrid, which is great for the LazyVGrid, it's not exactly what we
want in terms of the looks of our cards, but you
can see why LazyVGrid is taking this strategy. And there's another thing
to consider when it comes to these different
strategies between LazyVGrid and the HStack, which is
this Spacer right here. When we had an HStack here
instead of a LazyVGrid, the HStack was using all this space. So our Spacers just ended up being this tiny little sliver of
space between our buttons at the bottom. Here I'll make
it so we can select them, our buttons at the bottom here, and the HStack that was at the top. But now that we've used
the LazyVGrid here, it doesn't use all its vertical space. And so the Spacer has taken
up all the space in between, and that's the way Spacers work. They take any open spaces
available that no one else wants. So these buttons didn't
need any more space, the LazyVGrid, this is
all the space it needed. So there was extra space
and the Spacer took it up. If I take the Spacer out of here, I get a very different looking UI, because now these two things
combined to make this VStack. This is the VStack on the outside here, and this VStack no
longer needs that space. So it just centers what's in there. That's why we see this
VStack centered instead of using the whole space. When I put the Spacer
back, now there's somebody to absorb extra space, and so the VStack will be as large as possible. So this is a different
kind of spacing here than using padding which is fixed, and also
using spacing as an argument to your VStack here,
that's also fixed spacing. So it's flexible spacing that absorbs any extra available space
and that causes other Views like these and these, to pin to the edges. Just want to make sure you
understand the difference between using Spacers here, and just using padding or spacing. Okay, let's get back to making our cards look more like cards. What makes a card look
good when it's like this? Well, it's kind of two
wide for every three high. It's aspect ratio is 2:3 approximately, and I want my cards to look
like that 2 wide : 3 high. If you look at most cards, playing cards, all of these, they're generally about 2:3 in aspect ratio. This shows a little bit
of the power of SwiftUI how easy it is to make these
cards be 2 wide : 3 high. And that is with a ViewModifier
called aspectRatio. Just going to do 2/3 and has another little argument
here called contentMode. I'm going to do fit, won't
really talk too much about that. It's fit versus fill. That's just how it arranges
the space that it's given, but you can see that this
has immediately made all of these cards, be 2 wide
by 3 high in aspect ratio, still using all the width
available to the number of columns that we have right here, but it's making the
height match to be 2:3. So if I made a lot more columns here let's say we made six columns. I'm going to copy this and
paste a whole bunch more. Then again, they're still 2 wide : 3 high, but they're fitting six
columns right there. And the problem when we
only had three columns, the cards were a nice size, actually, maybe a little too big, nice size, but they didn't fit on the screen. In fact, they pushed our plus
and minus buttons completely off the screen. If we go back into preview mode, I can't even, I got too
many here, but let's go back to four and that doesn't even
fix the problem because watch. I have four cards, great. I'm
going to add some more cards. Add more cards. Add another card. Now
my minus button is gone. So clearly this is not good. This View is expanding to fit. And now it's smashing
into these other Views. So what do we do when we
have a really big View like this that can expand
and smash into other things? Well, we can put it in a ScrollView. A ScrollView is just another View like HStack or LazyVGrid
that contains other Views. So I just say, ScrollView, this in here. It'll be like that. Let's put this aspectRatio in
another line so it's clearer. Now I have these four cards and they're in a ScrollView. Now, if I hit plus, plus,
plus, plus, plus, plus, plus, it actually added the card down there. You see, plus, another card. And now we can remove and
add as many cards as we want. I'm going to take a second
now to explain this word lazy in our LazyVGrid. So a LazyVGrid is lazy about
accessing the body vars of all of its Views. We only get the value of a body var in a LazyVGrid for Views that
actually appear on screen, that scroll on screen. So this LazyVGrid could scale
to having thousands of cards, because in general, creating
Views is really lightweight. Usually a View is just a
few vars, like isFaceUp and content in our CardView, but accessing a View's
body is another story. That's going to create a
whole bunch of other Views, and potentially cause
some of their body vars to get accessed. So there's a lot of
infrastructure in SwiftUI to only access a View's body
var when absolutely necessary. This laziness we see in LazyVGrid. That's only a minor example of that. We'll learn a lot more next week about how and when the system
accesses a View's body var. This is looking really great. This is exactly what we
want, our cards are too big so we can scroll around on them. But I actually notice
a small problem here, which is that our card is
actually getting slightly cut off. This ScrollView cuts off
anything that doesn't fit in the scrollable area. And it's cutting the
edges of our card off. Now with the resolution of
the screen you're looking on you probably can't see that. So let's go down and make our lineWidth much larger, about 8. Now, hopefully you can
see that it's cutting off. I'll make it even larger, say 12. Why is it cutting off this edge? Well, it has to do with how stroke works. Stroke draws a fat line,
however fat you say there, 12 points in our case, draws
this fat line around right on top of the border. So some of the line is
spilling onto the inside of the shape, and some is
spilling on the outside. That's not actually what we want here. We want this entire border inside the boundaries of the Shape. And there's a great
function that does exactly that called strokeBorder. So this is a modifier that
strokes just like stroke does, but it strokes the border,
so stroking on the inside. Exactly what we want. Now we can go back to our
three, cards are looking good, they're not being cut off there. Let's go take a look and
see what it looks like in a light mode on our simulator. Make sure everything
is working over there. Here's the app. We can add
more cards, scroll around. How about landscape mode? Yes, there it is.
There's all of our cards. It's working here in landscape, but it doesn't look very good. These cards are way too big. If you look at this simulator over here, when I go to landscape
mode, it showed more cards. It's showing eight across right there, and only four in portrait. Whereas here we're showing
three, no matter what we're in, portrait or landscape. How the heck did I get this version of the app to show more cards
in landscape then in portrait? Well, there's a little trick we can do with LazyVGrid that tells it
to please plug as many things as you can fit across, before
you go to the next row. And here's how we do that
instead of having three GridItems where there's a fixed
three columns right here, I'm actually going to
have just one GridItem. Now this one GridItem that
I have, is going to have a little special
configuration, so that instead of putting one card per row, so there's this one card,
one column, one row. Instead, we're going to ask the LazyVGrid to take all these cards and pour them into this one column, as much as will fit, then it can go to the next row. It's kind of an odd way of
describing what's going on, but we're really having one column, and that one column
contains multiple items, and it just pours them in there. It pours them in there sort
of like text in a book, where the things that it's
pouring in are like words, and it just puts the words on,
gets to the end of the rope, goes to the next one, puts
more on, keeps going down to the next one. It also right justifies
the text so that it picks the width that exactly fits
like it was doing before. So how do we get our LazyVGrid to do this? Well, I'm going to take this
one GridItem right here, and instead of saying .fixed or .flexible, we're going to say .adaptive. Now, when it pours these
CardViews in there, LazyVGrid is going to want to put as
many of those cards in there as it can in each row. So we need to let it know the
minimum width we're willing to accept for each of our CardViews. We do that by just saying minimum, and let's pick something
like 25, let's say. It took our four cards
and poured them in there, and made them a small as possible, but not smaller than 25,
but 25 is clearly too small. Let's put more cards in
there and see what happens, go to 20 cards and resume. There we go, put 10 per row, There're essentially wrapping
again, like text in a book but that 25 is too small.
Let's see what happens if we go to 100 as the minimum width. Well at 100, we're back to
three cards in each row. And obviously our cards aren't fitting. We're having to do the ScrollView. So that's a little too big,
let's try something bigger. 65 looks pretty good. That's 20 cards at least,
fits nicely. If we went to 24, we'd be back to scrolling, that's certainly better. What about landscape mode? Let's go back to our simulator over here, and rotate this thing into landscape. Ooh, look at that one, two, three four, five, six, seven, eight, nine. It puts nine across. So we get nine, 18, 20. It's got a ScrollView here luckily, because it's hard to pick
a width that works well in both portrait and landscape. What we really want to do
is pick our width based on how much screen real
state is being offered to us. We'll learn how to do that later next week or the week after, but
pronounced stick was 65 and we'll just rely on
our ScrollView here. if we have two in landscape
or if there's too many cards. Okay, that is it for this week. Next week, we will dive
into the game logic for our memorize game, see you then. - For more, please visit
us at stanford.edu.