Lecture 2: Learning more about SwiftUI

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
(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.
Info
Channel: Stanford
Views: 77,186
Rating: undefined out of 5
Keywords: Swift, SwiftUI, Xcode, iOS, iPhone, iPad, Stanford, CS193p, coding, iOS programming, Memorize, emoji, LazyVGrid, GridItem, HStack, onTapGesture, ForEach, Spacer, Button
Id: 3lahkdHEhW8
Channel Id: undefined
Length: 85min 4sec (5104 seconds)
Published: Mon May 17 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.