Lecture 4: Memorize Game Logic

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
- [Narrator] Stanford University. - [Instructor] We are back for lecture four. Stanford CS193p, spring of 2021. We're going to start right off with the demo and complete what we started with this MVVM architecture. We've done the Model, we've done the ViewModel. Now we need to hook all that up to our View. After we get that working, I'm gonna come back to the slides briefly to talk about two things. The first is another type called enum and then this special version of enum called Optional. Probably one of the most important enums, if not the most important enum in all of Swift. Then we'll go back to the demo briefly to finally make it so that our game actually plays the memory matching. But first, we need to finish up our MVVM architecture. So let's get back to that part of the demo. In lecture three, we built our Model and our ViewModel for our Memorize game. Next step we're gonna do is hook these things up to our View. Our View currently is essentially demoware. It's showing emojis directly. I'm gonna get rid of all of this demo. including this @State isFaceUp which we use to temporarily flip the card over using this tap gesture. All of that now needs to happen in the Model. It's in the Model that we're keeping track of whether cards are face up, not in @State in our CardView over here. And we're not going to be ForEach'ing over the emojis anymore. We're gonna ForEach over the cards. Recall from the lecture that one definition of var body is "give me a UI that shows me what's in the Model". Our ContentView here needs to be able to see the Model so it can give out a UI that draws the Model. We're gonna do that by adding a var viewModel which is of type EmojiMemoryGame. Now you're always gonna have a var like this at the top levels. When you're showing what's in the Model. Of course, you have to be able to see your ViewModel. Probably not gonna call it viewModel. Likely this would be called something like game. I'm gonna leave it called viewModel so that as I use it in here, it will be clear to you, oh, we're accessing the ViewModel there. But we can't just add a var like this to our ContentView, without setting its value. We could do an = here but we generally don't do that. If we think of a View as just an agent for showing what in the Model through the ViewModel, then more likely, we're gonna wanna pass it to it as an argument. So I'm not gonna set it equal to something here. And instead, everywhere we create a ContentView, I'm going to pass it in a ViewModel. Where do we do that? Well, the main place we do that is in our app, the main program. Remember this, here's where we create the ContentView. It's going to show our entire UI. I'm just going to create a little var here let game = EmojiMemoryGame(). Now this is an interesting line of code here. I'm creating an EmojiMemoryGame by just doing open parentheses, close parentheses. That's because of the free init that classes get. structs get a free init that's even better. It initializes all the vars. But classes only get a free init that does nothing. That's okay though because the only var we have, we initialize it here. So that works. If that wasn't the case, we'd have to make our own init here in a class. So we're using that free init to create our emoji game. The other thing, notice I made it a let. You might be a little concerned about this because clearly, this emoji game is gonna vary. We're going to be flipping cards over and doing matches and all kinds of stuff. So how can I make this game be a let, a constant? Well, remember that this is a class, it's a reference type. This game is a pointer to it. And when I say let game, I just mean I'm not gonna change this pointer, but what it points to, I'm definitely gonna change that. Classes, like our ViewModel are mutable, can always be changed through the pointer. That's what wonderful about them. Imagine what's terrible about them because it's dangerous. Anybody who has appointed to it can change it. But it works nicely here 'cause we want our ViewModels possibly to be shared amongst a lot of different Views, not in our current app, it's simple, but we might down the road. So I created a game here. I'm just gonna pass it as our ViewModel to our ContentView. That's going to set this variable right here but there's actually another place we create ContentViews you don't wanna forget about that. Down at the bottom of this file, our Previews. We even have two Previews, light and dark mode. So we wanna do the same thing here. I'm gonna create a game and I'm gonna pass it along as the ViewModel, pass the same ViewModel to both of them. Keep in mind, this is a pointer, these are shared, no reason two ContentViews can't be looking at the same ViewModel. And so now we're totally hooked up here. Our ContentView has been hooked up to some EmojiMemoryGame to show. And now, we just need to make our var body do its job and show it. How's it gonna do that? Well, let's start with the cards. Right here, we wanna ForEach over to all the cards. So I'm gonna say ForEach(viewModel.cards, .... We used to have emoji here but now, we're going to ForEach over the cards themselves and we'll have to create a CardView based on that. This is ... ... this in our ViewModel Just a var cards returns an Array<MemoryGame<String>>. And it's easy to implement. It's just returning the Models cards. And yes, we don't need to return there. And the Model's cards is this a private(set) var. It's a private(set). We can get the cards in our ViewModel. We can't set them directly but we can get them and return them through our own var to our View. Remember, we could have done private(set) right here and then not had this cards at all. And if we did it this way, and instead of having this, then in our View, we would be saying viewModel.model.cards. Whether you do it this way where you're exposing the Model directly and letting people look at the cards in the View, or whether you do it this way that we originally did it over here, kind of an "art of programming". Depends a little bit on what kind of Model this is, whether it's just a data store type Model or whether it's got quite a bit of logic. I tend to be a fan, at least at the start, of making my Model completely private and exposing it via vars and funcs to my View. But if over the long-term, I decide that this is just unnecessary overhead I might go back to doing private(set) here. We're making great progress here. When we are ForEach'ing through the cards, of course, this is no longer "emoji in", this is "card in" because we're going through an Array of Cards here. And our CardView is no longer being passed the emoji as content. Instead, our CardView is essentially now redefined as a View that shows what a card looks like. So I'm gonna add a var to this called card, which is a MemoryGame<String>.Card. And this CardView takes a Card and just builds a body for it. This is also a var body that is just showing what's in the Model. Notice I haven't passed the entire Model to it. This is not passing the ViewModel to it so it can access the entire Model. I'm just passing a tiny slice, tiny little sliver of the Model. One card, and this is highly recommended. When you build a View, only pass into it the minimum it needs to do what its job is. CardView's job is build me a UI that shows a card in the Model. Only pass it the card. Also, this does not want to be a var. These are read only structs. This card cannot change anyway. So don't make it a var. The only time you're gonna have vars is if you're using @State. And we said, @State very rare in a View. So you're not gonna have a lot of vars in your Views. How about this ViewModel? We'll make it a let now as well, although that is going to change as you'll see in the future. So we're ForEach'ing through the cards. Each time we got a card, we're gonna say card: card. Here's a case where you might be saying, hmm, luxury redundant to me. I really wanna do this underbar thing so that I'm not having this external name be card: card, this is obviously a card. And we could do that. The way we would do that, get rid of this card because that's coming from here is to add an init to our View. Views can have an init, they're just structs. Then we could have an init where the first argument is underbarred so that you don't have to specify it. But it's a trade-off here between the complexity of adding more code down here and making this look slightly better maybe by not having the word card here. I prefer not to add the init here. Now that we're passing a card into our CardView, things like isFaceUp and content come out of that card. Viola, we have a CardView, no errors. It's displaying this card, building a UI for this card. And we have our ContentView which is using the ViewModel to go through the cards. We're down to one error left. Look at that one error. It's this generic struct ForEach requires that MemoryGame<String>.Card conform to Hashable. Holy cow, what does that mean? We know that conform to is "behaves like". So why is it saying that? Well, we left in this little id: \.self. We were using self in our emoji strings as our unique identifier. Remember that everything in the cards Array has to be uniquely Identifiable. So we were using self when they were strings. We certainly do not want that here. Our Cards want to be Identifiable on their own. Now I expected when I removed that to get a nice error here that said card must behave like Identifiable, which we actually got when we originally put strings in there. But sometimes the Swift compiler is unable to figure out a complex expression. And in this case, for some reason, it cannot and it even says failed to produce diagnostic. Please submit a bug report. So it wants to use submit it. But luckily in this case and this can be frustrating when you get this 'cause you're like, I can't see what the problem is. What's the problem? Help me compiler, and it's not helping you. But luckily, in this case, I know that the problem is that these Cards aren't Identifiable and they need to be be 'cause anytime you do ForEach over an Array of something, it needs to be Identifiable. How do we make our Cards Identifiable? Well, let's go back to where our Cards are defined, right here, this struct and Identifiable is just a behavior just like we have our ContentView behaves like a View. We're gonna make our cards behave like an Identifiable. What the heck is an Identifiable? Super simple. It is just a single var that is used to identify this struct against all other Card structs Now we know that when we do "behaves like", that it's a double-edged sword. One, we get this nice ability to identify ourselves, but two, we might have some responsibilities. And with Identifiable, you definitely do. With View, our responsibility to behave like a View as we had to do our body. With Identifiable, we're gonna find out what the requirement is by going here to the error and clicking fix. Fix is saying, do you want to add protocol stubs? Okay, we're gonna learn about protocol stubs next week. But protocol stub basically is saying, what are the vars and functions necessary for me to say I behave like an Identifiable. So let's say, fix and see. Oh, it added this one var right here. var id, its type is ObjectIdentifier. This is really a "don't care". And if we go look at the definition of Identifiable, we're gonna find out that this can be any type we want since it's a don't care, although it is restricted to having to be Hashable and Equatable. And that just means we can look it up in a hash table. We want things that are Identifiable to be able to look up in hash tables. So I could pick anything. This could be a String, those are certainly Hashable. It could be an Int that's definitely Hashable. For me, I want an Int because I have a great idea how to make my cards unique. And I'm gonna show you in a minute. And viola, my card is Identifiable. No errors. I'm gonna put this id down at the bottom. It's not really as important as all my other vars. That's why I want it at the end. And we're almost there. We do have an error here though. Missing argument for parameter "id" in call. Oh yeah, because I didn't set this equal to anything. So now when I create a Card, I have to both specify its content and I have to specify the id. Well, here's my great idea. I'm gonna have the unique identifier be the pairIndex * 2. I'm gonna have the id of this second card in the pair be pairIndex * 2 + 1. Now, all my cards have a unique identifier. As I go through the pairIndex's here creating all my cards they're each gonna get a unique id. Now we can identify a Card from any other Card. Now that these Cards are Identifiable, if we go back here, no more error, build succeeded, run. And there's our UI. And indeed, it's exactly reflecting what's in the Model through the ViewModel. We've got one, two, three, four pairs of cards. Go to our ViewModel. We see how many pairs of cards or and we're also seeing that the cards are all face up. Why are all the cards face up? 'Cause in our Model, we have isFaceUp default to true. If we change isFaceUp to default to false and run, now all the cards are face down. And if we went back to our Model and said, oh, give me only three pairs of cards instead of four and run, we get three pairs of card. Clearly our ContentView is giving us a UI that reflects what in the Model. Now you might think the fact that I was going back to the simulator all the time means that once we hook up our Model and ViewModel, we can only use this simulator now. No, we can indeed go over here and bring back our Preview. And we can resume and see it here. Make it smaller. And again, we can change things. Go back here to our MemoryGame. Let's change this back to four. Oh, I lost my Preview. I guess if I go back here, oh, there it is. And we'll resume. And there's four cards, but this is a little bit annoying that every time I go back to my ViewModel or to my Model, I lose my Preview. So let me show you a trick for that. We're gonna go back to our View here and click this pin. This little pin says, keep this guy on screen as I go to my ViewModel and my Model. So that if I change, for example, the cards back to isFaceUp = true and then resume, the cards are all face up. We're back to my ViewModel. Again, I'll make this back to three pairs of cars. Instantaneously shows me the cards. This is fantastic. We have got this nice UI. I'm gonna fix one other thing in my UI that's bothering me by the way, which is this VStack. This VStack was used be holding those buttons along the bottom. Don't need that. Let's get rid of this VStack, make our UI even cleaner. What about flipping the cards over? Tapping on the cards to flip them over. I'll put this in live Preview mode here and resume. Nope, no flipping over the cards. It doesn't work in our simulator either. Tap, tap, tap, nothing. Why is this not happening? Well, because our Model is responsible for flipping these cards over, that's part of playing the game. That flipping needs to happen when you choose a card. So we're gonna fix choose to start out anyway by flipping the cards over. Eventually, it's gonna play the entire game in here but first, we wanna flip it over but we have to hook up this choose to our UI somehow. We took away that onTapGesture down here that was just diddling with @State. So we needed to put it onTapGesture back somewhere here and have it hookup to the Model. Let me do that right here. CardView, we can say onTapGesture. And inside here, I want to express the user's Intent to choose a card. This is where those Intents come in. And Intents are all recorded in the ViewModel. I even put a comment (gonna to use this // MARK: comment which I'll show you) where I'll put my Intent and I might have multiple of them here. This is where I'm gonna put the functions that let me express user Intent. This // MARK: - is so that when you go up here, lets you go right to it. Just put a little header right here. So there's only one user Intent that our game has. And that is the Intent to choose a card. And our cards are MemoryGame<String>.Cards. That's their type. This Intent is something that we gotta hook the UI directly up to. Our UI is gonna express this Intent directly by asking the ViewModel to choose this card. Card that's in our ForEach the card that we're drawing. We're gonna choose it. This is what the code would look like generally in your View, asking ViewModel to express this user's Intent. Implementing this is trivial in our simple little game because our Model lets us choose a card directly. This is a one-liner. Not all Models are so accommodating to their ViewModels. This might be quite a complicated series of instructions to modify a database or go out in the network and do something. Whatever, it all depends on how complicated our Model is. In our simple case, we can just do one line, call our Model's choose and get the job done. Let's check and see if we've successfully wired up the user's Intent through our ViewModel and it's making it to our Model by adding a print statement. print is a great way of debugging. We haven't even talked about the debugger. We're actually not gonna talk about it too much but print statements are super way to just print out what the state of your application is at any time. They only work with the simulator, work with the Preview here. So I'm gonna run the simulator and I'm gonna tap on a card and I'm gonna hope that it prints hello somewhere. Uh-oh, it's not doing it. Well, it actually is doing it. It's doing it down here in the console. This console you have to drag up. And again, oh no, I don't see hello in here. Well, this is actually a shared space between the debugger which is what you see here and the console over here. We can divide the space up and use these buttons to hide one or the other. So don't be fooled about, oh, I put print statements in, they're not printing. This is where they are gonna show up. There's even better way to deal with this. If this is down into like, oh, I printed what's happening. You go up to the Preferences menu in Xcode. Whole lot of preferences in Xcode. I'm going to go to this Behaviors one here and this Generates output. This is essentially saying, what is the behavior of Xcode when the simulator generates output? I'm gonna go in here and pick show the debugger with the console. It could show both of the variables and console. That's the debugger and the console but I'm gonna do just the console. So now if I'm running my program and this thing is down and it generates some output, which it did already even before I clicked a card, it brings this up. So you're almost certainly gonna wanna set that. Otherwise, you forget, the prints will be printing and you won't be able to see it. Whatever do I click on a helicopter? Hello, hello, hello, hello. So it is working. Our UI has been hooked up. The user's Intent is being reflected here in the Model. But we want something more than just hello to happen here when we choose, we want to flip this card over. Now you might think this is a simple matter of just saying card.isFaceUp equals not card.isFaceUp. By the way, doing this, taking a Bool and making it equal to not the Bool there's actually a function in Bool called toggle which should do exactly that. Toggle, toggles this Bool. When I do this, oh no. Cannot use mutating member on immutable value. card is a let. It's saying I can't call toggle on this Bool because card is a let and indeed, this card is this card. And all arguments to functions are lets. They are constants. Which makes sense in a way, passing a card in, especially passing structs that are copied. You want them to be lets. So where is the card we actually wanna change? It's in here. This is the Array of our Cards. This is the Cards we need to change. So we've gotta somehow find this Card in this Array and then change the Card in this Array to have it face up. How are we gonna do that? I'm going to say let chosenIndex = I'm gonna make up a function here, index of this card and have to write a function here called index(of:) func index of card. It's gonna return an Int which is an index into this Array. So we'll return zero till we go implement this. And once I have the chosenIndex then maybe I could say var chosenCard equals our cards Array at the chosenIndex. And then maybe I can say chosenCard.isFaceup.toggle. Maybe this'll work. Seems to compile. It's probably good. We can even verify it's working by printing the card out. So how do we print this chosenCard out? chosenCard equals ... in other languages you might say %s or something and then have some string here. We don't do any of that in Swift. Swift has an awesome feature which is \(). In the middle of a string that you are printing, you can put \() and in here, put anything you want that will evaluate to a String. And most things in Swift, will evaluate to Strings. Boolean, Int, Strings, even whole struct. If I put the whole chosenCard struct in here, this can evaluate to a String because this struct is only made up of things that can evaluate to a String. Even our CardContent is gonna be a String. So let's just fix this index(of:) to actually find this card in our Array. And we'll be good to go. We do this with a simple for loop. I'm gonna say for index in 0..< my cards count. If cards at that index dot id equals, um, "of" (that's my argument right here, "of.id") then I'm gonna return the index. And if we get through this foor loop and don't fail, then guess what? We return zero. That's the first card, that's bogus. We'll fix that later. Also, I don't really like "of". "of" is kind of a bad variable name here. So that's why we want to be able to say "of card". Have internal name and external name that are different. And we say "card" there. Do I want this to be "of" or _ here? Would I rather say chosenIndex = index(card) or would I rather say chosenIndex = index(of: card)? And I think the latter. Even though by the rules, putting _ here would probably be legal because this index is taking something that is clearly a card so it would be redundant to say card but it's not necessarily redundant to say "of" if it makes this code read much nicer. We want our code be readable. So let chosenIndex = index(of: card) reads pretty smooth. So it's worth the preposition here. Let's give our app a run and see what's working. We see our console appearing. Let's try this first helicopter. Boom. This is really great. Look how it has printed out our chosenCard as a String. card.isFaceUp: false, isMatched: false, content: the helicopter, id 4. It's taken all of our vars converted them to a String form and then added it all up into a nice String that shows our whole card. And it even seems to be working, card.isFaceUp false. That's good. Our helicopter was face up when we touched on it but this is not actually working. And to tell you about that, we're gonna have to go take a look at our cards. I'm gonna print the chosenCard but then I'm gonna print out all my cards as well. \(cards) That's gonna print all my cards. Let's try that and see what we got here. I'm gonna click on this helicopter again. Boom. Let's see what we got. Helicopter is false. Oh no. All my cards are all at face up. face up, face up, face up, face up. Even both helicopters. Why are the Array of cards not being changed when clearly it looks like the chosenCard got changed to face down. Well, I did this intentionally so that you really understand about structs. When I say var chosenCard = cards[chosenIndex], it makes a copy of this card. Even just assigning one variable to another, copies things. structs are copied around all over the place. If you wanna change this card at the chosenIndex, you have to change it directly. We have to say, instead of having this var chosenCard, we have to put this cards[chosenIndex] right here and get rid of this whole chosenCard intermediary which was a copy. So here, I'm taking the isFaceUp that's in the cards Array itself and toggling it. But that has an error as well. Cannot use mutating member on immutable value. 'self' is immutable. Oh, no. 'self' is my whole card. You're telling me that my whole card is immutable? And by default, yes it is. And if we have a function like choose, that's going to mutate a card. We need to let the world know by saying mutating. Putting mutating in front of a func tells the world that calling this function is going to change this thing. Therefore, this function can only be called on a Model (a MemoryGame) that is a var. If we said private let model right here, then we wouldn't be able to call choose down here cannot use mutating member on immutable value. You see? So this is how Swift knows to do that copy-on-write. And how it enforces the mutability of things. We have to say this function can mutate, can change this struct. So we build that. It's gonna get rid of our error. Now let's run. Let's see if we're actually changing our cards. Same helicopter, boom. It looks like it worked. True, true, true, true. That helicopter, false. Nice. Except my UI didn't flip over. What? How come my UI didn't show this base, let's try another card. Let's try the bicycle. It's faced down too, they're both faced down. I'm not seeing anything. What is the problem? This is where all the work we've done this week culminates in a very simple mechanism that makes our UI reactive. All we need to do is make sure that every time this Model changes, the UI rebuilds. Pay close attention here. This is where MVVM really has its full payoff. We're gonna add three simple but powerful keywords to our code and turn our UI into a fully reactive one where any change to our Model is instantly going to cause the entire UI to update to match. So how do we do this? We start with our ViewModel. We're going to make it behave like something it's gonna behave like an ObservableObject. ObservableObjects are the kind of objects that can publish to the world: "something changed". When you say something behaves like an ObservableObject, it gets a var that you can't really see called objectWillChange. It's of a certain type. a don't care, essentially. We don't care what this is because we're not going to actually define this objectWillChange. It's not like body var where we have to implement it. It gets implemented for us, but we get it for free. And we can use this objectWillChange to cause our ViewModel to announce something has changed whenever our Model changes. So for example, when you choose down here, you can say objectWillChange. And the way you make the objectWillChange publish that. If you say .send. This says, send to the world that this objectWillChange. Its objectWillChange by the way, because maybe multiple things will change. And SwiftUI is great at batching them all up into one redraw on screen. So that's why we want objectWillChange. Again, we don't need this objectWillChange. You can call objectWillChange send anytime you want. Anytime you change your Model and you wanna advertise to the whole world that something changed, you can call objectWillChange.send. And we certainly would wanna do that here 'cause we're changing our Model. We want to let everyone know the Model changed. However, it's even a little cooler than that. We can take any of our vars and put another keyword @Published in front of them. And in this case, anytime this changes, anytime anyone does anything that changes the Model, it'll automatically do this for us. So we don't even need that because model.choose choose is a mutating func. So SwiftUI knows that that changed it. And we will automatically do objectWillChange.send. This is part one of our MVVM notifications to make it so that our ViewModel is doing its job to publish to the world "something changed" every time its Model changes. The second part of it is having it so that our Views redraw when something changes in their ViewModel. And to make that happen, we need to put another little keyword here which is @ObservedObject. @ObservedObject means that when this says something changed, please rebuild my entire body. That's all @ObservedObject means. We have a little error here, it says property wrapper can only be applied to a var. So we do have to make our ViewModels var because these property wrappers, we're gonna talk all about property wrappers next week or the week after but they require that these have to be a var. Amazingly, that's all there is. Once we have these @ObservedObjects and we're observing our ViewModel which is an ObservableObject and is going to be publishing "something changed" every time that the Model changes, now our UI every time we change it, like change isFaceUp, it's gonna redraw. Let's take a look. Well, our favorite helicopter. Amazing. I clicked it again, click all of them. They're working. Flipping the cards over. And that is it. That is MVVM. And this whole thing will work in our Preview as well in live mode. We'll resume. Very nice. That's it for this part of the demo, we're gonna go back to the slides and talk about enum and Optional. And after that, we'll come back here. We'll flesh out our choose to not just flip a card over but to actually play our full Memorize game. enums are kind of like structs and classes and that they're building block data structures. However, enums only have discrete states. The value of an enum is one of some number of discrete values. For example, here, I've created an enum fast food menu item. And at our fast food place, we only have four items. Hamburger, fries, drink, and a cookie that's it. So an enum is a good data structure to represent that because there's no other options. Now, an enum, just so you know right off the bat, is like a struct in that it is a value type. So as I pass it as a parameter, or I set one variable equal to another variable this thing is being copied around. Copied. We're not about reference types here. What's amazing or interesting, notable about enums in Swift is that each state can have some associated data with it. For example, in our fast food menu item, maybe the hamburger has the number of patties. Is it a single burger or a double burger? And the fries have the size of the order and the drink has the what kind of drink it is. How big is it? And cookie has none. We maybe only have chocolate chip cookies so we don't have any associated data. Now in this example I'm giving you see the size is type FryOrderSize. That could be an enum too. case large and case small fries. Also, I want you to note that in the drink case, it's got String and ounces: Int and then the String case is unnamed. But we didn't say that that drink String is like the brand. Although that is what it is. We probably should have labeled it. But I just wanted to show here that it doesn't have to have a label, each of these associated data and that you can have multiple ones there too. Drink has both a String and this ounces, which is an Int. So all the items in our enumeration can if they want to have this associated data. So how do you set the value of an enum. We know what an enum looks like to declare it. How do you set some variable to have a value? Well, you just say equals <name of the enum> dot the case. So FastFoodMenuItem.hamburger or FastFoodMenuItem.cookie. That's how you set it equal to it. And if it has associated data, you just put it in parentheses there with the argument. If it had one, patties: 2. Now my menu item there, that menu item constant I have is a double burger. You can leave off the FastFoodMenuItem. from one or the other side of the equals, but not both. So you can say menu item equals FastFoodMenuItem.hamburger or you can say other items of type FastFoodMenuItem equals cookie but you can't say you had another item equals cookie without FastFoodMenuItem being mentioned in there because that .cookie might be on some other enum. There's just no way for Swift to know what you mean there. So now we know how to declare an enum. We know how to set its value. How about checking the value? If I have a var, how do I check its value? We do that with switch statements. Again, other languages have switch statements. Swift has commandeered the switch statement to be the way we go and check the value of an enum. We can use if statements if there's no associated data involved. But most of the time we're gonna use switch statements to figure out which state our particular enum is in. For example in this case, it's gonna print out burger because our menu item is a hamburger, a double and I'm switching on it and case hamburger, it prints burger. So you just say switch, your enum variable and then case this, case that, case that, case the next thing. That's how you figure out which one it is. And for each one of them, you can do whatever code you want. Here, I'm just doing print, but you could do arbitrarily complex code there. Because Swift knows that you are switching on a fast food menu item here, you don't have to say case FastFoodMenuItem.hamburger. FastFoodMenuItem.fries, FastFoodMenuItem.drink. You can just leave that part off. This is again, part of Swift's type inference. This is an obvious thing for it to be able to do for you. If you have one of your states, hamburger or fries and you don't wanna do anything just don't want to print out, for example, in that case. You can say break. break breaks out of the switch right there. But it's really important to understand though that you have to handle every single case in your switch. You cannot switch on something and then not handle every case. However, there's a really nice case called default and the default case will catch all the other cases you didn't specify. So in this particular case here where we have our menu item: it's a cookie, it's gonna go through this switch. It's not a hamburger, it's not fries. So it's just gonna be the default case. It's gonna print other. Switch works on other types by the way. It doesn't work just for enums. You can switch on a String if you want it to. Say case goodbye to something, case hello to something. But if you're switching on something like a String, you have to have that default because remember, you have to switch on every possible case and a String has an infinite number of cases. So you have to provide default when you're switching on something open-ended like a String. And in each of your cases, they don't have to be one liners like print hamburger. They could be multiple liners. But one thing to note, if you make multiple line ones, they don't fallthrough to the next case. Whether you have one line or multiple lines, they don't fallthrough. If you want fallthrough behavior, there is a keyword fallthrough that you could put. For example, after where it says print fries, it would fallthrough in there and it would fallthrough into the drink case, but we don't usually do that. In Swift, I don't really recommend it. It's anti-Swift to do it that way. But people coming from other languages where things do fallthrough, maybe wanna do it that way. So they make, they preserve the option by having this fallthrough keyword. But what if our enum has associated data? Like how do we get the patty count and the size of the fries and the kind of drink it is? Well, we do that. Same thing. Switch statement looks exactly the same, except in each case, we're gonna put parentheses let and some variable name. And that is going to have the associated data and put it in a variable of that name for us to use in the code we're executing for that case. So for example, in this case right here, we have our menu item is a Coke, 32 oz coke. It's gonna print 32 oz coke on the console 'cause we're gonna get to the drink case. It's gonna have let brand and let ounces are gonna grab each of the two pieces of associated data for that. And when we print, we're using the ounces and brand variables. So we just defined with those lets to print it out. You're allowed to use different names here than are declared in the enum. When I declared this enum, I used the variable name patties for the number of patties in the hamburger and for brand, I didn't even put a label for the String or the brand on a drink but when it comes time to use it, I can pick any names I want to make my code look nice right here. enums like structs and classes are allowed to have methods on them. Perfectly legal. And they can have vars but only computed vars like var body where you drop in the function. They can't have vars that have any actual storage because an enum storage is its cases. And if you're a case hamburger with a number of patties, that is the storage. The patties, and the fact that it's a hamburger, that's all the can be stored in an enum. And enum represents discrete value. So you can't have additional stored vars that you throw on there. If you needed additional stored vars or something like that, you would put your enum as a var inside a struct. If you do add a method to your enum, you might need to execute some code inside that function which depends on what kind of enum value you are. And you can do that by doing switch self. Here, I have a function is included in special order. Number five or number three or number two which is a function on a fast food menu item. I can ask any menu item, are you included in special order number two? And it executes this function. While inside there, I'm switching on myself. If I'm a hamburger, then special order number one let's say is a single burger. And special order number two is a double burger. So I'm gonna see if the pattie count that I have is that number. And then for fries and cookies, I'm saying that in every special order. And for drink, I'm only gonna say it's included in the order. If it's a 16 oz drink, all the specials are 16 oz drinks. One other little piece of info I wanna glean from this slide is that notice when I was checking to see if a drink is included in a special order, I didn't care what brand it is. So you can have a Coke, diet Coke, Pepsi, whatever in your special order. So to get at the second associated value, I just put _. Remember _ is always the placeholder for anything can go here. We're not paying any attention to it. So I just put that _ there. So I could put a comma and get to the second one. That's what we can use underbar for when we're getting the values, the associated values of an enum. How about if we wanted to enumerate through all the cases of an enum? We wanna use a for in and just like we can for in over an Array, it would be really cool to be able to for in over all the possible values in your enum. Hamburger, fries, drink, whatever. So here I have an enum on Tesla Model and it's got Model X, Model S, Model 3, Model Y. And I wanna be able to iterate over it. I can do that as long as I mark my enum as behaves like CaseIterable. Remember colon and protocol listed there. Is a way to say that this thing behaves like. So if an enum behaves like a CaseIterable, then it will have a static called allCases. allCases is just something you can iterate over with for in and each time you go through the for in, you're going to be having the next case in the enum. We've already seen an enum with associated values in our demo so far. It was when we were creating that LazyVGrid. Remember when we created a LazyVGrid, we had to provide the Array of GridItems that describe how our LazyVGrid is laid out, how many columns it has, etc.? And we did some different things there. We said, GridItem.fixed(100). And we didn't really do this, but we know the default is a GridItem that's .flexible. And of course, in the end, we ended up using the GridItem which is .adaptive(minimum: 65). That's what our demo code currently says. Well, all of those were just cases in the enum size inside the struct GridItem. The full name of that adaptive thing we put was GridItem.Size.adaptive(minimum: 65). That was the full expression of the enum case that we use to say, which kind of GridItem we want. And this is perfect fit here because there's only three different ways to specify how to do your LazyVGrid's columns. And these are what they are. Adaptive, fixed and flexible. So anytime you have only X number of ways, three ways or five ways or two ways, you want an enum but each way needs different information. Fixed wants to know what is the number to fix it at. Adaptive and flexible both wanna know the minimum and maximum that they can live within. Also notice here, some of the cases have defaults. That's why we were able to say .adaptive(minimum: 65). We didn't have to say maximum something because the maximum there is defaulting to .infinity. .infinity by the way is just a var, static var, type var on CGFloat, which essentially represent infinity. And you can see also here that the fixed case didn't put a label on the float because if you say fixed 20, it's obvious you mean fixed at 20 points wide. So this is a great real-world example of an enum that has cases with associated data. Now, I'm gonna show you another enum that has a case with an associated value. Probably the most important enum in all of Swift is called Optional. And Optional is a cause for great consternation when students are first learning Swift, but an Optional is just an enum. Really nothing more than that. It looks like this. It's an enum with two cases, the case none and the case some. It's also a generic type, just like our MemoryGame with generic, or like an Array is generic. It's generic over any type you want. So that T right there can be any type. And that type is the associated data or associated value on the some case. Again, just like we had number of patties on a hamburger being associated value. So the some case has an associated value which is that generic type. So where do we use this? We use it any time we have a variable whose value can sometimes be "not set" or unspecified or undetermined, something like that. These variables are sometimes gonna be in the of this .none case of this enum. And they're sometimes going to be in the state where they're set and they're gonna have a certain value. That's just gonna be the .some case of our enum with that associated value. And this happens surprisingly often. In fact, it happens so often the Swift has introduced a lot of really good syntactic sugar to make it really easy to use this enum up here. So let's take a look at this syntactic sugar because that's the key to really understanding Optionals is understanding how this syntactic sugar maps to this Optional enum we have here. Let's first talk about declaring a variable to be of type Optional. In other words, we want a variable whose type is this enum right here. You do that with a question mark. You just specify the type T, the generic type String or Int or whatever, with a question mark after it. So below you see, I have this var hello, that is of type Optional String. It's not a String. When new students first see var hello: String? They think, oh, hello is a String. But no, hello is an enum. Hello is an Optional. String is just the type of the associated value that comes with that Optional when it's in the set state. Really get used to saying when you see this kind of code. var hello is an Optional String. Now, once you've declared a variable hello to be an Optional String, you can set its value. And there's two different values it can have because it's an enum with two cases. The first value is the "not set" case .none. And we use the keyword nil to represent that. When you see the word keyword nil in Swift, it means Optional.none. The none case of Optional. You see the very bottom line of this slide, var hello: String? = nil. We are setting that thing to be the .none case of our Optional. The other thing you can say there too is the value you want the associated data for the some case to be. And so this syntactic sugar that's what provides is you can just say var hello which is an Optional String equals "hello". That's saying set hello, which is an enum to the some case with "hello" as its associated value. And that's it. That's the only thing we can set an Optional to. Either we set it to nil, which is none. Or we set it to the some case by specifying what we want the associated value to be. One thing that's kinda cool about Optionals is that when you declare them like this, you don't have to say equals nil to start them off. Optionals always start off with the .none case as their default value. So you see where I've put the little circle there, the little oval, there's no equals nil there. That's perfectly legal. We know that in Swift, all variables have to have a value from the very time they're initially created and that's true for Optionals as well. It's just that Optional start out in the .none case. Again, that's just syntactic sugar. It's something that the Swift compiler is doing for us to make our lives easier. So we know how to declare an Optional and how to set its value to either nil or to something. How do we get the value out? How do we look at the value, this associated value when we have an Optional? The syntactic sugar for that, there's two different kinds of ways to do it. One is exclamation point. If I have a var hello and I say, hello! that means if this Optional is in the some state, give me the associated value. That thing of type T. If this Optional is in the none state, crash my program. That's what exclamation point means. That's what we call for forced unwrapping. Or unwrapping Optional, looking in there, grabbing its some associated value, but if it's none, we crash. We're forcing it out of there. This is exactly the same as what you see on the right side of the screen there, we're switching on hello. hello is just enum. It's an Optional, so it's an enum. And in the case of none, we crash the program. But in the case of some, we use let data to get the data out of there. And then we print it, cause that's what's happening on the left arm thing. Print hello! Now some of you are saying, thank you for telling me about exclamation point. I'll be sure to never use it because I don't want my program to ever crash. But no, we want our program to crash when we're in development if we have a hello at some point in our code and we are certain that it's never supposed to be not set at that point, it's always supposed to be in the some case, then put an exclamation point there. And if it crashes, it will crash in development, you'll find a bug, that won't go out to your customers. If you work around it and it don't let it crash, then your customers could get that really wonky behavior out there. Because you told me you are certain that hello is not supposed to be not set right there. However, there are certainly many cases in your code where it's perfectly valid for an Optional to be in either the none case or the some case, depending on other circumstances. So the other way to get the value of an Optional is with if let. This is really elegant syntax here, you're combining let because you're gonna declare a new variable here to have the value you want out of the Optional. And if too, because you're testing whether the Optional is in the some case, together to get the value. All I say is if let safehello, which is a new variable, equal hello, my existing Optional, then print safehello. Otherwise, do something else. And in the other something else place, I'm not gonna access safehello because my Optional was not set. This is just like saying switch on hello, the enum and in the case of none, we're gonna do the something else part. And in the case of some, we're gonna use let data to get it out of there. And then we're gonna do what we wanna do. In this case, print that data. These are the two ways we're going to get information out of an Optional. Either force it out of there and crash if it's not set or we're gonna use if let to safely extract it. These last two slides, the one where we're declaring and setting the value of Optional and here where we're gaining the value, that's 90% of knowing how to use Optionals. There are a couple other minor pieces of syntactic sugar that we're gonna take a look at but they're much less important. If you understand this slide in the previous slide, you're gonna have Optionals. So the first minor syntactic sugar is the question mark. Question mark operator, it does Optional defaulting, essentially. It's called the nil-coalescing operator. This operator is gonna have something on the left side of it, and something on the right side of it. On the left side is gonna be an Optional. And the right side is the value to use if the Optional is not set. So that's just like switching on that Optional. And in the case of there's none, use the thing on the right side of the question mark, question mark. In the case of that it's some, use it's value, the Optional's value. Really nice way to just have a default value of your Optional happens to be not set. Another piece of syntactic sugar is Optional chaining. Here, we're gonna use the question mark when we're accessing the Optional. Not when we're declaring it, when we're accessing it. And what this allows us to do is go through a chain of things that are Optionals. And each time we get farther and farther, we keep going. And if we ever hit something that's nil, we just give nil back to the thing we're trying to assign. So here, I'm letting y equal this expression. I'm starting off with x, which is my Optional there. And if it's set, then I'm gonna call the foo function on it, which also returns an Optional. And if that returned something that's set, then I'm gonna access the bar var on that thing. And if that is in the set state, then I'm gonna access the z on that. And if any of those things ever returned nil, then y just gets to be nil. So that looks like this. I'm switching and switching and switching and switching. So first I'm switching on the x then I'm switching on the value of x, then I'm switching on the value of that foo function. Then I'm switching on the value of the bar var all the way down to getting the value of z. And if anywhere along the way, something comes back as not set. Boom, y gets to be nil. So that's called optional chaining. Again, kind of Optional use of the syntactic sugar. Believe me, you will start using that by the end of the quarter but early on, that might be a little too much to grok here. So don't worry too much about that. All right. The best way to probably really get these Optionals is to see them in action. So we're gonna go back to our demo now and we're gonna do two things with Optionals. One, we're gonna fix that bogus index(of:) method that was returning zero. In other words, the first card. If it couldn't find the card we were looking for, that is bogus. Of course it wants to return not set or undetermined. So we're gonna make that return an Optional. Then we're gonna go back to making our Memorize game actually play that game of Memorize and using Optional in the logic is going to figure prominently there. So seeing both of these things using Optionals hopefully will give you a much more of a feel of how we use these things to our advantage when we're building things in Swift. So let's hop right back into that right now. We've got our application hooked up beautifully now between our View over here, our ViewModel and our Model. but our game only flips the cards over when we tap on them. We need to make it so that it actually plays the game. But before we do that, we're gonna fix this bogus return value down here. Let's remind ourselves what this function does and why this is bogus. When we choose a card, we need to find out which card in the Array we just chose by looking at the identifier, the unique identifier of the card and comparing it to all the cards that are in our Array. So that's what this function does. Just as a for loop that goes through all of our cards and find the one that has the same identifier. But if it goes through here and they can't find the card that matches it, maybe someone passed us a card from a different game or something like that. Something that doesn't have an identifier that matches, then we return zero. Now, this is bogus because that means that if we can't find a card, we're just gonna return the first card. And that clearly makes no sense. That poor first card did nothing to deserve being the return value if we can't find a card. So what do we wanna return here? We essentially wanna return "we couldn't find the card you're looking for". And we're gonna do that by changing our return type from being Int to being an Optional. This Optional Int can be nil or in the .none case, if we can't find this. So instead of returning zero, we're gonna return nil. Otherwise, we can just return an Int and Swift is automatically going to turn that into an Optional enum in the .some case with an associated value of this and it does all that for us. But that breaks our code over here of course, because here, I have a cards Array and I'm trying to index it by something that is not an Int. I just option clicked here to see what type it is. And the type is Optional. Not Int. This is not the type Int, this is the type Optional who's associated value is an Int. We clearly can't index in Array with an Optional. We have to index it with an Int. We learned two ways to take the return value of this index here, this Optional and turn it into an Int. So we can actually do it. One way is with exclamation point. If you put an exclamation point after any expression that is an Optional, it will look at it, assume it's in the .some case, grab the associated value and use that instead. And if it's not in the .some case, it crashes your app. So this line of code will crash my app if this index cannot find the card and returns a nil. I could also put this exclamation point right here. I can put the exclamation point either where I got it or I can do it where I'm using it. It doesn't really matter. Any expression that is an Optional, like chosenIndex. chosenIndex is an Optional can be exclamation pointed. But now, this line of code will crash if this function could not find the index and returned nil, the .none case. clearly here, we do not want to crash if we can't find the card because this func choose, this is a public method. It's not private. So any code anywhere in our app could call it and they could pass a card here that is no good. And we don't wanna crash in that case. We just wanna ignore it. They're saying, choose a card that's not an Array. We just ignore it. Maybe we might put an error out on the console or something. We'll talk a little how to do that later in the quarter. But we certainly don't want to crash here. So we're not gonna use the exclamation point. Instead, we're gonna do it the other way which is if let. That version is just a matter of typing if in front of the let we already have and then putting some curly braces around the code you wanna execute if this Optional is not nil, if it's in the .some case. Our chosenIndex only will be set to something. If this is not nil and we're only gonna do what's in this curly braces if this was in the some case. chosenIndex, this var doesn't even exist outside here. It only exists inside this curly brace. Being able to do if let is just part of the syntactic sugar that Swift provides to handle Optionals. We're almost ready here to start playing the game instead of just flipping cards over. But before we do that, I wanna show you that this function actually exists in Array. This might be a little hard to believe but you can do this function in Array. We didn't even need this code right here. And that's something you're gonna find in Swift. And one of the reasons you really have to familiarize yourself with the programming interfaces, especially of Array, is that there are a lot of functions that will do what you want built in to this Swift library. Let's take a look at how we could use a function in Array to do this index lookup. And to make some more room here for us to maneuver, we'll unpin our Preview there. We're also going to copy this line of code and comment it out so that you can see what the old code looked like when I go in here and replace it with the new code. The function we want in Array that does this looking something up is called firstIndex(where:). Functional programming strikes again here. This little function takes a card. It's gonna be called repeatedly once for each thing in the Array. And it returns a Bool whether that's the droid you're looking for. That's what the where means. And it's called first index. Because even though our Array can't have two cards with the same identifier, that's the whole point of them is that how they're identified. You could have an Array of something else where that's not true. And so, there could be multiple matches. This is gonna return the first index where this function is true. Notice the little throws in the middle right there? That's an air handling thing that we're probably not gonna talk about until week six or something like that. Not important in this particular case, you can ignore it. So we need a function here that takes a card. One of the cards in the Array and in our case, compares it against the identifier of this card and returns whether it was found. Open curly brace. I'm gonna make the argument to my function. Be called aCardInTheCardsArray. And then I'm gonna check to see if that aCardInTheCardsArray's id equals my card's id. This is the card that was passed to me. So this already reads kind of like English. I'm gonna look in the cards. I find the first index where that card's id equals my card's id, but we can make this look even more Englishy because you're gonna replace the arguments to a closure with $0, and $1, $2, $3 for the other arguments and get rid of the Int and just say $0.id equals the card.id. And now this reads as we're gonna look through our cards to find the first index where that card's id equals our card's id. Even more Englishy. Now this expression right here replaces all of this and all of this. Much cleaner code here. And now the moment has come where we can actually make our game play. To go back to our View and pin the UI again. Let's work out a strategy here for the implementation of how we're gonna play our matching game. First, obviously we want all our cards to start face down. Oops, they all start face up. So let's go make them be face down and resume. If we choose a card, nothing really happens. We have no matching going on. But if we choose another card, now we have to match. If we choose a third card, I want these cards to go face down. And we choose this card, they stay up. They'll have to be matched another card to go face down. Now, if I choose two cards that match and then click on another card, I'd still want these to go face down and preferably I'd like them to disappear. Just go out of the game. But I certainly don't want to be able to click on them again and have them come back up. I already matched them. That's how my game plays. Now, the key in all of this is knowing when we have to do a match. And that only happens when we click on a card and there's already one and only one card face up. So if I click on this card, I have to try and match it right here. But if I click on this card, I don't have to do any matching. I just have to turn these face down. No matching required here. So I'm gonna start my game logic by adding a private var which I'm gonna call the indexOfTheOneAndOnlyFaceUpCard. An Int 'cause it is an index into this Array. And you might think there's a kind of a crazy name indexOfTheOneAndOnlyFaceUpCard and it might be a little bit longer than it needs to be. We might be able to come up with a nicer, shorter name but I'm only gonna have to type it this once. After this, Xcode is going to escape complete it for me everywhere. I could always rename it later. So my logic here really only has to do two things. It has to keep this var up to date as the cards go up and down. And when there is one and only one face of card and I flip a card up, I have to try and match them. That is pretty much the entirety of my logic. Now I added this and we have an error. It says return from initializers without initializing all stored properties. Oh, you know what that means? You have a var, it's not set equal to anything. What do we want this var to start off with? When we start our game and all the cards are face down, what is the index of the one and only face up card? Oh, oops. There is no indexOfTheOneAndOnlyFaceUpCard. So this is essentially not set. Well, what kind of type do we know? That's good for storing things that are sometimes not set? An Optional of course. We'll make an Optional. And when there's not one and only one face up card, this will just be nil. It'll be in the .none state. Notice that that got rid of my error because we get a free equals nil for all Optionals. Any type of an Optional you get equals no for free. We don't need to say it. It is initialized. This variable does start out initialized. It's initialized to Optional<Int>.none. Also known as nil. Now our logic is just to see if there is one of these and if there is try and match it. So I'm gonna say if I can let potentialMatchIndex equal the indexOfTheOneAndOnlyFaceUpCard 'cause that's the only time I have a potential match is if there's one and only one face up card. Then I'm gonna ask and see if the cards at my chosenIndex, that's the one that user just chose. If its content matches the potentialMatchIndex content. If it does, I have a match and I can just say that my chosenCard is matched and the potentialMatchIndex'ed card is also matched. Whether, I got a match or not, there's clearly two cards face up right here. So the indexOfTheOneAndOnlyFaceUpCard now has to be nil. What if there wasn't one and only one face up card when we chose this card? That's the else case right here. In that case, we need to turn all the cards face down because either they're already all faced down or there's two cards face up that either match or don't match, they need to be turned face down. So how do we turn all our cards face down? for index in 0..<cards.count. cards[Index].isFaceUp equals false. This will turn all our cards face down. Now the card we just chose is gonna get turned face back up because I left in this isFaceUp.toggle. We're still gonna flip that card face up. No matter what we do. When we click on a card that's faced down, we wanna flip it up. While we're here, I'll talk about cool little thing you can do. If you find yourself saying 0..<array.count, you're gonna replace it with the Array's indices var. This indices var on every Array return to this Range here of 0..<cards.count. It looks a little nicer to do this, not a huge difference but it's kinda nice. In this case, I just turned all these cards face down until the chosenCard that I just chose is gonna be turned face up. So that means that the indexOfTheOneAndOnlyFaceUpCard is the I just chose. And this is the entire logic of playing this game. There is nothing more to it really than that. The only last couple of things I wanna do is that if you click on a card, that's already face up, I wanna do nothing. When we were doing testing, we had clicking on the card and turn it face up, face down, face up, face down. But in the real game, if you click on our card this already face up, you don't turn it face down. It just does nothing. So here where I get the chosenIndex, I'm going also check and I can do that with and that the card at the chosenIndex dot isFaceUp is not true. So I'm only gonna do all this stuff if I'm clicking on a card that was face down and I'm turning it face up. But we have an error here. It says, cannot find chosenIndex in scope. I just said, if let chosenIndex right there, chosenIndex Well, it turns out you can't use and in an if statement, if you start out with the let. But you can use a comma. If you have an if statement, you can separate the terms of it with commas and it means the same thing as and except for that it will actually do this if let, before it does this one. And so chosenIndex will be defined here. The other thing is if I touch on a card that's already been matched. I don't wanna do any of this matching either. So I'm also gonna say, and not cards[chosenIndex] dot isMatched. Choosing cards that are already face up or that are already matched, is not gonna do all this matching business. It just ignores it. Now this line of code got really long, started wrapping. We can make it look a little nicer by just putting each of the comma terms of this expression on their own line. I think to look a little more readable here. We're almost there. This final error here. What's this say? Binary operator equals equals. So this cannot be applied to two CardContent operands. I'm trying to see if the CardContent on one card is equal to the CardContent on the other. Oh no. The equals equals not working on CardContent. Well, what type is CardContent? It's this don't care. This CardContent could be an arbitrarily complex data structure. So it won't necessarily just be able to be =='ed. How are we gonna deal with this because we want this to be a don't care but we also wanna be able to do ==. I'm gonna give you a preview of next week where we're going to to talk about how to take a don't care and turn it into a "well, I actually do care a little bit". We do that with a where clause. Well, I'm gonna say where CardContent behaves like Equatable. This Equatable behavior means you can do == on it. We're gonna talk all about this next week in detail but it's the same kind of thing as behaves like a View or behaves like an Identifiable. In this case, our don't care type has to behave like an Equatable. It has to do ==. I've turned this don't care into a "I mostly don't care". We're doing that all over the place in Swift. This is really a heart of this protocol-oriented way of design that Swift supports. For example, back in our ContentView, look at ForEach. What is its argument here? That is actually a don't care. It could pay an Array of anything here. However, that Array of anything needs to be things that are Identifiable. So you can be sure that ForEach, when it's defined, it defines as don't care with a where clause that says the items are behaving like Identifiable. And that's exactly what we're doing here. We're making our don't care behave like an Equatable. So you simply can't create a MemoryGame with some data structure that does not have Equatable CardContent. It won't work. Really no such MemoryGame exists for creating a card game where the content is not Equatable. Now we're done. Let's take a look. Let's see if it's working, resume. Choose this card. Okay, that's good. Now indexOfTheOneAndOnlyFaceUpCard is presumably index zero. And this card. All right, there was an indexOfTheOneAndOnlyFaceUpCard We tried to match it, it didn't match. indexOfTheOneAndOnlyFaceUpCard is now nil, not set. And that's right, because we have two cards face up. So now when we click here, oh, it turned those back down 'cause we got in here indexOfTheOneAndOnlyFaceUpCard was nil so this if left did not do this, instead, it did the else and the else turned all the cards face down. And of course the chosen, when it got turned, face up. Now our indexOfTheOneAndOnlyFaceUpCard is this chosenIndex. So that if I click this one, it does the matching again. In this case, hopefully it's set isMatched to true on both of them. Now we'll choose another card. It turned those face down, excellent. Now I'm gonna try and choose one of these. Oh, I'm tapping on it. It's not doing anything. That's because we don't allow tapping on cards that are already matched. See that? If we chose an index and it's not matched. That's good. Although I don't really like them hanging out here by themselves. We'll fix that in a second. Let's keep going with our logic here. Got this one face up. I'm gonna click this one. They don't match. This one, these go face down. This one, this one, these go face down and here, a match. Click those go face down and they're matched. So I kind of click here. I'm clicking, clicking, clicking, nothing, click here. Now the only thing I didn't like about this was that my matched cards are still showing on the screen, even though they're already matched or kind of out of the game. Be better if they just didn't appear. So let's go back to our View and have our cards draw a little bit differently if the card is matched. The way I'm gonna do that is I'm gonna say else if the card.isMatched, then draw the shape opacity zero. .opacity is a View modifier that says is how transparent the entire View should be. .opacity(1) is fully opaque. .opacity(0) is fully transparent. So by drawing the shape with opacity zero, I'm still taking up the space that a shape would use so that my cards don't move around a little bit when they disappear. But it's gonna show nothing just literally transparent here. Let's see if that works. We'll resume. And we'll click here. And the helicopter, bicycle, they're gone. This one, no. Here, here, gone. Excellent, that is it. We have completed our game. We've reached the finish line here. And just like I did last week, I'm gonna do a quick zoom through review of everything we did in lecture this week. We learned all about MVVM. We learned how to make Models like this one. We found out that these Models are UI-independent. That's why we only import Foundation. And why there's no reference to anything in the Views or the ViewModel for that matter in our Model code. Our Models have our data like all the cards and our logic in this case, the choose function which is doing the matching and in your homework, you're gonna have it add scoring and things like that. We also learned that the Model is of the truth. All the information about the cards, whether they're face up, whether they're matched, et cetera, is all here in the Model. It's not stored anywhere else. Then we learned that the purpose of our View that we've been building is to reflect the Model. These var body: some View, the definition of this variable is give me a UI that reflects the current state of the Model. We also were able to get rid of our @State that we had in our CardView 'cause we learned that we don't store state in our Views. We store them in the Model. One really great thing we found out about our Views is that they're declarative. So ScrollView, here it is. The LazyVGrid here it is. The cards, here they are. As our code unfolds, so does our View. What we declare here is what appears here. And what we see here is completely reactive to what's going on in our Model. And we accomplish all that by introducing our ViewModel which binds the View to the Model. This ViewModel can also serve as an interpreter. Now, our ViewModel is very simple. It doesn't really do much interpretation as it provides information from the Model to the View but it could, if this was a very complicated Model, this might be very complicated code in here. ViewModel is also the gatekeeper for Model to the View. It makes the Model private and exposes information about the Model to the Views only as they need. The ViewModel also enables the entire reactive architecture. It does this because it notices changes in the Model. And by the way, I haven't emphasized this enough, I think. The reason that we're able to detect changes in our Model is because MemoryGame is a struct and Swift is able to detect changes in structs. You can't do it in classes but it can detect changes in structs. So that's how our ViewModel knows when something changes in our Model. Our ViewModel then publishes anytime it sees a change here. And it does that because it behaves like an ObservableObject which means it has this hidden variable objectWillChange. Where you can say send on it. And this send notifies the world that this has changed. We also learned that we don't actually have to explicitly do that if we just @Published any of our Model variables. @Published means when this changes call objectWillChange dot send, let the world know something has changed. Then our View, because it marks its viewModel var as an @ObservedObject, we'll get its body rebuilt. This happens automatically by SwiftUI. And it happens efficiently. Only Views who actually changed in here will have their body rebuilt. So @ObservedObject changing. We'll rebuild this body, but when it comes to rebuilding the body of this ScrollView and this LazyVGrid and these CardViews, it depends on whether they've actually changed. For example, our CardView takes the card. Here it is. These are both structs. The CardView itself is a struct. This is a struct. Swift can tell if structs have changed. If these have not actually changed as a result of the something change that came from the ViewModel, then this body will not get rebuilt. And this is where Views are expensive. Views are not expensive just to create the View with this one tiny little var. They're only expensive if you actually rebuild their body. But we can take advantage of the fact that Swift knows whether structs have changed to not rebuild the body if nothing has actually changed in a specific card. Finally, we learned that when we tap on a card or otherwise do some multitouch event in here, the way that that gets back to our Model is through these Intent functions. So when we get a tap gesture, we express our Intent as a user to choose a card in our ViewModel. And our ViewModel in turn, figures out what to do in the Model. In our case, we have a simple Model so it's just calling choose in the Model as well to get the Model to change. And the Models choose, which is a mutating function because it's going to change the Model, does whatever it needs to do, its logic, et cetera. And all the changes that makes in here might cause its cards to be changed, which causes the MemoryGame struct to be changed, which gets noticed here because Swift notices when structs change, which gets @Published, which causes objectWillChange to happen with the ObservableObject, which causes our @ObservedObject ViewModel to cause this body to get rebuilt. And we're gonna recreate all these CardViews but only build the bodies of the ones that have actually changed. And that is MVVM in a nutshell. And that's all we have for this week in your homework. You're gonna replicate everything that we've done again. And just like last week, you'll add some stuff to make sure you really understand what's going on in here. Next week, we will learn a lot more about the Swift language. You're still learning that. And of course, we'll learn much more about SwiftUI and the powerful things it can do. And in next week's homework, not this week's homework but next week's homework, you will be on your own to write your own card game app from scratch. - [Narrator] For more, please visit us at stanford.edu.
Info
Channel: Stanford
Views: 42,394
Rating: undefined out of 5
Keywords: Optional, nil, force unwrapping, optional defaulting, nil-coalescing, enum, Memorize, MVVM, View, console, objectwillchange, ObservableObject, ObservedObject game logic, Equatable
Id: oWZOFSYS5GE
Channel Id: undefined
Length: 91min 20sec (5480 seconds)
Published: Mon May 24 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.