(gentle music)
- [Narrator] Stanford University.
- [Instructor] We are back, lecture four.
Stanford CS193p, spring of 2020.
Today, I'm going to start
off with a big demo,
and it's going to be to make our card game
be in rows and columns, instead
of all across in one row,
which is gonna make our
game a lot better, no doubt,
but it's also gonna be
a super good example
of doing generics with protocols,
and also functions as types.
And we're gonna learn more
about being a container View
because we're gonna build our own
rows and columns container
View for our cards.
After that demo, we're gonna
hop back into the slides here.
We're gonna talk more about
the Swift type systems,
specifically the type enum.
And then after that, we're
gonna continue in the slides
and talk about one of
the most important types
in all of Swift, Optional,
which happens to be an enum.
And Optional is so
important that I'm gonna go
right back into a demo after that
and show you two really important examples
of using an Optional to
design your code in Swift.
So, let's get started with that demo.
All right, well, it's type
to fix this major deficiency
in our game, which is
that all of the cards
are in one row,
and we know that we could
much more efficiently
use this space if we had rows and columns.
We're gonna do that by
replacing our HStack here
with a Grid.
Now as of the taping of this class,
there was no such thing
as a Grid in SwiftUI,
so we're gonna have to write that.
And it actually provides
us with a great opportunity
to learn a lot about how
things like ZStack work.
So, ZStack we know takes
this argument right here,
which is a function.
This curly brace means it's a function.
It's a function that takes no arguments,
unlike for example, ForEach,
its function to create a View
takes an argument.
But ZStack, the View that gets built here
is quite powerful.
It could be a list of other Views.
It could be a if-then,
a combination thereof.
So this particular function that can build
these complicated View
is called a ViewBuilder.
And we're not gonna use
ViewBuilder quite yet.
We'll eventually learn about ViewBuilder.
So our Grid is just
gonna have a simple View
that it's going to
replicate using a ForEach,
exactly the same way as a ForEach,
to put a certain View at every spot
in the row and column.
So our Grid is really going
to combine HStack like Grid,
except for 2D HStack if
you wanna think of it,
with ForEach like this.
We're gonna take an Array
of Identifiable things,
like these Cards, and then
we're going to pass a function
that takes one of the Identifiable
things as its argument,
and returns the View to use
to draw at that location in the Grid.
Very simple here.
So let's go create that View.
We're gonna go over here to New, File.
We're gonna put it in its own
file because this is really
a very powerful reusable object.
We could use it in all our
apps that needed a Grid.
So it is a SwiftUI View,
so I'm gonna click there.
I'm gonna call it Grid.
Gonna double-check that
it's in one of these
yellow folders here and that this content
kinda matches that, and
it does, so we're good.
And here's our Grid, and of
course, we get "Hello, World!"
This code down here, you'll
remember, at the bottom
is to hook us up to our canvas over here.
But this Grid is completely generic.
And so, if we were gonna
make this preview work,
we would need to come up
with some test data for it.
And we're not gonna do that in this demo,
but someday in the future,
maybe you could do it
as an exercise.
But if you ever delete
this and need it back,
it'll say Create Preview
here in your canvas.
All right, so we gotta
get started on Grid.
First thing we're gonna
do is its two arguments.
The first argument is this
Array of Identifiable,
and the second argument is this function
that takes one of the
Identifiables and provides a View.
So let's get those two
arguments there as vars.
So the first var is I'll call
it items, an Array of Item.
And this Item for us in
Grid is a don't-care.
We really don't-care what that thing is.
It could be anything.
It's gonna be a Card over
here, but there's no reason
for it to be any particular thing,
so it's a don't-care for us.
And similarly, that second argument,
viewForItem I'll call
it, that's a function.
It's a function that takes
an Item as the argument,
and it returns some ItemView,
which is another don't-care for us.
We really don't-care what
kind of View you provide
for each item, so that's
a don't-care, ItemView.
Good start, and we could actually
get this going over here.
Unfortunately, we have to
name this items right there.
This one is really viewForItem,
and put that right there.
But this is the last
argument to this function,
so we know that when the last argument
to a function is another function,
then we can just get rid of
that label, close it off,
and have our function
kinda float on the outside.
So this is the last argument to our Grid.
And that is both of its vars,
this one and the viewForItem.
Just like when we called CardView here,
we had to initialize its
vars as this one Card var,
same thing here with Grid.
We're initializing its two vars.
This might be a good time
to talk about how is it
that ForEach doesn't have
to have this label items?
When we had ForEach, it just
had the iteratable thing
that had Identifiables in it.
How come it didn't have an argument?
Well, it's using technology
that you know well to do that.
So let's do it in Grid as well,
which is it's just an init.
So normally you might
have your init say items
is an Array of Item,
viewForItem is a function
that takes an Item and
returns some ItemView.
That would be a normal kind of init.
And in here, you just want to
initialize your vars, like items.
Well, that's equal to the
Items that are passed in.
And then viewForItem, that's
equal to the viewForItem
that's passed in.
By the way, Swift is gonna
be very confused here
because it doesn't know
which items this is
and which items this is.
We've got a local argument to
this function called items,
and then we have a property called items.
So you can easily fix
Swift's confusion here
by saying self.items,
and self.viewForItem.
This is a different reason
to have to put self here
than we saw with the explicit
self blah-blah-blah hit fix.
But it makes sense here, right?
By doing self.items, now
Swift knows that we're talking
about the green one,
and that this one must be the black one.
Now, this allows us to put this under-bar
as the external name.
Remember under-bar is external name
means do not provide an external name,
and that's exactly what's going on here.
And if I rebuild, you're gonna see
that this code over here, perfectly fine.
However, we still have a
problem back here in our Grid.
This error right here.
It says, "Assigning non-escaping parameter
"'viewForItem' to an @escaping closure."
So I'm gonna try and explain
this escaping closure
in just a few minutes.
Normally, I would actually
probably have slides on it.
I'm gonna skip that this
quarter because in SwiftUI,
since we're doing functional programming,
almost everything is a value type,
and the problem that this
escaping closure thing
is trying to address here
really, really rare in SwiftUI.
Happened a lot more when we
had object-oriented programming
in UIkit.
So, here's what's going on.
This function that's passed in here
that creates a View for a given item
is not actually used in this initializer.
We salt it away into a
var and call it later.
We're gonna call it down here in our body
when we need to actually create the Views
for all of our Items.
So, we have to mark this
kind of function, @escaping.
You can think of it as this function
is going to escape from this initializer
without getting called.
Now, Swift has to be careful,
and it's actually very powerful
and knows how to deal with these functions
that might get called later.
Why can that be an issue?
Well, let's look back
here where we actually
pass this function in.
Whatever code is in here has
to be able to be called later.
So if we use any variables,
especially if we use
a local variable or
something from this function,
that has to be around in the future
when this function gets executed.
Now, how does Swift
accomplish this wonderful feat
of keeping everything in
here around until the future?
Well, it does this by
making function types
be reference types.
Just like classes, our
ViewModel are reference types,
they live in the heap.
They're stored in memory.
People have pointers to them.
Same thing with these functions
that can be called later.
They live in the heap and
they have pointers to them.
Now, the things inside of here
might also live in the heap.
If any of the things in here
are classes, for example,
then the self in here
is gonna be a pointer
to something in the heap.
And the problem we're trying
to avoid here in Swift
is having self have some var in it
that actually points to this function
because we know this function
points to self inside here.
They're both gonna be in the heap.
The function's kept in the heap
because it's gonna be executed later.
All the things inside the
function have be kept in the heap
so that it will properly execute later.
And so if anything in here points back,
we've got a situation where
two things in the heap
are pointing to each other.
And the way that Swift cleans up memory
is when nobody points
to something anymore,
it cleans up the memory and frees it up
for someone else to use.
Well, if two things are
pointing to each other
and they're both in the heap,
they're never gonna be able to go away
because they're always gonna
have a pointer to each other.
That's called a memory cycle.
So, this whole thing is to try to make it
so that we can detect memory cycles
by seeing these escaping functions.
Amazingly, this is also why you
get this warning about self.
You know when we don't
put a self-dot in here
in our onTapGesture function,
we're gonna get this error that says
this requires explicit self
to make the capture semantics explicit.
Capture semantics means
the fact that it's going
to capture everything in here,
inside this onTapGesture's function,
and keep it in the heap
so that when onTapGesture
executes it in the future when
someone taps on this Card,
that this stuff is still around.
Well if you look at this code,
you're thinking, "Oh well, I
guess it captures ViewModel,"
but it actually would
have to capture self.
So it makes you type the self
so that you realize, oh yes,
this function is going to
make self be in memory.
And then you can verify
that self doesn't actually
somehow directly or
indirectly come back around
and point to this function,
which it doesn't do in
this case because in here,
our ViewModel never points to our Views.
Views point to the ViewModel
but not backwards direction,
so it's no problem.
But even more it's no
problem because this self-dot
does not live in the heap anyway.
This self-dot is this
struct, self is this struct.
Structs are value types.
They don't live in the heap.
So, this is not necessary anyway.
And that is the fix that
has been publicly approved,
that you're probably gonna
see a couple of months
after this video is made.
So this is gonna be out of
date quickly, this video.
But the fix is basically
if self, in this case,
inside of one of these escaping functions
is gonna be held around in the heap,
if self doesn't live in the heap,
in other words, it's a value
type, a struct or an enum,
then you don't need to
have this error come up
and warn you, "Hey, put self-dot there
"so you make sure you
don't have a memory cycle
"where self points to this function,
"this function points to self."
That can't happen because
there aren't two things
in the heap that could point to each other
because self is a struct.
It's a value type, does
not live in the heap.
So, there you got just a
little quick explanation
of all this escaping, and
you even understand now
what this self-dot is.
If you don't understand
all of things I just said
about these escaping
functions and this self-dot,
really, I don't think you have
to worry about it that much
for a couple reasons.
One, this week's reading,
you're gonna read
about these closures, these functions
that are in-line that
capture the stuff they need
to be able to execute later.
So when you read that,
maybe you'll understand it.
Of course, you always have the option,
go on the class forums, ask
more about it to clarify.
And finally, with SwiftUI,
it's just not that important
because so many of the
self-dots are just value types.
Everything in the View, for
example, is all value types.
Our ViewModel, it's a class.
It has possibilities.
But since the ViewModel
never points to anything
in the View, we never really have to worry
about anything in the View
creating these memory cycles.
It'd be very rare.
It's possible to do, but very, very rare.
So the bottom line is I
don't think you have to worry
about it that much.
The only thing that's gonna bother you
is that you're always
gonna have to put self-dot
in front of these things,
or you're gonna have
to deal with this error
and do this, click on
it, and fix all the time.
Again, that's something
you're only gonna have to do
till the beta of this
public thing comes out,
and hopefully, in a couple of months.
But, at worst, a few months.
And after that, then you
won't have to worry about this
at all because not putting self here
when self is a value type will
not generate this warning.
That is the proposed fix.
All right, that's enough of that aside.
This all started because
we had this warning
that told us we had to put escaping here
because indeed, this function
does escape from this init
because we put it in this var.
Let's do our body.
Our body's not "Hello, World!" obviously.
Our body is actually quite simple.
It's just a ForEach of all of our Items.
And for each of our Items,
we are going to return
a viewForItem of that Item.
We're gonna call our function viewForItem.
This is a var, that's a
function we're gonna call it.
And of course, we wanna ForEach.
It also has an escaping
function there for the View,
so we have to say self-dot.
This creates some errors for
us, so let's look at these.
The first one says, "Cannot
convert value of type
"Array of Item to expected
argument type 'Range of Int'."
Oh no, we saw this exact argument problem
when we were over here doing
ForEach the first time.
We were passing this Array of Cards
and we were like, oh, I
thought I could do that.
We used to pass zero dot dot less than for
and we went to Array,
and I had told you that
this ForEach takes an Array.
But we learned that it was an Array
only of Identifiable things.
So we have the same exact problem here.
This ForEach of this
items has to be an Array
of Identifiable things.
Well, that's a problem for us
because Item is a don't-care.
We have no idea what this thing is.
But here is where we get
constrains and gains into the act.
We're gonna say where
Item is Identifiable.
So now we've created
a Grid that only works
with this don't-care when that
don't-care is Identifiable.
So this can still be anything.
But it has to be Identifiable.
This is what I was talking
about in the slides,
a care-a-little-bit kind of thing.
We care a little bit about Item.
We care that it's Identifiable.
So now we have a different error here.
This ForEach error is saying,
"Return type of property
'body'," this is our body,
"requires that 'ItemView'
conform to 'View'."
So what is ItemView?
That's the return type of this function.
And of course, that makes sense
because ForEach can only
use Views to have Views
for these items.
This has to be a View.
So, this viewForItem return
type here of ItemView,
it has to be a View.
And what type is it?
It's a don't-care as well.
We'll do the same thing here.
ItemView has to be a View.
You all seeing how we're
connecting generics,
this is a very generic struct
right here, with protocols.
These are protocols, and
we're using them to constrain
these don't-cares to work.
The next thing we have to do
here is we're a container.
Grid contains all these Views.
It puts them in rows and columns.
It contains them.
And we know the container's
job is to take the space
that's offered to them and divide it up
amongst the things inside.
That means we need to
figure out how much space
has been allocated to us.
And we know how to do that.
That's GeometryReader.
Okay, GeometryReader.
Geometry in is going to allow us
to find out how much space
was given to the Grid,
which we're then going to
hand off to these guys.
So, put this in here.
Now, as soon as put it in here,
of course we're gonna get the
self-dot problem with items.
I'm gonna use the same
solution I used last time,
which is I'm gonna
create a func here called
body(for size: CGSize).
It's gonna return some View of course.
I'm gonna put my code
for my body inside here.
And here I'm just gonna do a
self.body(for: geometry.size).
Again, this is just purely to make it
so I don't have to do
self-dot inside this body.
Just like GeometryReader is escaping
and so we did this self.body,
ForEach is also escaping,
and so I'm gonna do the same thing here.
self.body(for item, in size).
I'm gonna move this to
another little func
body(for item, in size)
turn some View.
And now I don't need the self-dot here.
Just return that.
So, we've arranged our code nicely here.
All we have left to do, believe it or not,
is to actually offer these
Views some of our space,
and then to position them.
That's what containers do.
Offer space, and they position.
To do this, we need to do some math
because we've been offered this space,
and now we need to divide
it up by however many items
we have here.
And to do that math, I
actually wrote some code
that I gave you, and we're gonna use here.
Let's drag it in.
By the way, when you drag code in,
be sure to do this Copy items if needed.
That's almost always what you want.
You want a copy of that
code to put in your project.
Otherwise, it's going to be
referring to your desktop
or wherever you dragged it from.
All right, so we finished that.
So here let's take a look at this little.
This is the math, this GridLayout.
The math, it does this calculation,
so let's take a look.
Now I've collapsed the init
and this two var and func here
'cause we're not gonna look at
the implementation of these.
I do encourage you, after this lecture,
to go and take a look
at the implementation,
and see if you can figure it out.
You'll need the rest of this
lecture to have the knowledge
to be able to really
understand what this is doing,
but it's a good exercise to
try and understand this thing.
It's only about 10 lines of code,
so you shouldn't have
too much trouble with it.
But here's what a GridLayout does.
Its initializer takes a
number of items and a size,
and it divides that
size up by those items.
It does exactly what Grid wants.
Now, it also even lets you
specify a desired aspect ratio.
It can't guarantee that, but
it can try and get close to it.
And that's it, we just create
a GridLayout that does that.
Then we have a var here
that will tell us the size
of the items that are all the same size,
so this would be the size.
And then also the location
of each item as a point.
This is the center of each item,
which again, exactly what Grid wants.
So GridLayout was definitely
made with Grid in mind,
but it's pretty much a
generic divider-up of CGSize
by a certain number of items.
So, let's create this GridLayout
by calling this initializer right here.
I'm gonna do it up in my GeometryReader.
In fact, I'm gonna change
this passing of the size
to instead pass a GridLayout.
And the itemCount is
just the number of Items
that we have.
This is our items Array
right here, its count.
And I'm gonna skip the
desired aspect ratio thing.
We'll just use whatever the default is.
Hopefully in your reading,
you saw that you can have
default arguments for functions
and GridLayout uses that.
But you do have to have the size,
and this is of course going
to be in geometry.size.
And we're still inside
of GeometryReader here
so this items is gonna
have to be self.items.
It's an instance variable there.
So, I've changed this body(for).
Instead of body for size, it's
now our body for the layout.
GridLayout here.
And same thing, I'm gonna
have to pass this on through,
so this is now body(for:
item, in: layout).
So that makes this a layout as well,
passing it on down along the line.
And now we're in here,
we've got this layout,
and we've laid it out with the
number of Items in our size.
So we can easily do things like
offer space to these Views,
which we do with frame.
So we're gonna say frame's width
is the layout's itemSize.width,
and the frame's height is
the layout.itemSize.height.
We're gonna offer the same little subspace
of our space to each of the items.
That's what itemSize in GridLayout does.
Again, GridLayout was
created with this many items
in the size that was offered to us.
That's how we divided it up.
And then we have to position the View,
so we do that with position.
That's the function we do it with.
And we're gonna position
it at the layout's location
ofItemAt index.
So this is indexed, just like
we had with our Cards in fact.
So we have to find the index of this Item
in our Item Array.
No problem, we kinda know how to do that.
We'll say let index equal,
mm, I know how to do this.
I'm gonna create a function
on my self index of this Item.
And I'm gonna go down here
and say func index (of item).
And it's gonna return an
Int, an index into the Array.
We'll return zero here first,
and then we'll fill it out in a second.
Now, this is gonna confuse
Swift again a little bit
'cause you've got index,
this local variable,
and then index, this function.
So, we can fix that
again, same way, self-dot.
Now it knows this index is the function,
and this index is a local
variable we're creating.
So, we do actually use self-dot.
Even once they make that
change with the value type
self-dots not being flagged like that,
we still sometimes wanna use self-dot
to define the difference between
something that's a property
or a function versus a local variable
or a parameter to the function.
All right, in index here, how
are we gonna find this Item
in the Array?
Well, guess what?
These Items are Identifiable.
And probably some of you are like,
"Hmm, this seems awfully familiar."
Well yeah, we're gonna do for index
in zero dot dot less than items.count
that we have.
And if the items at that [index].id
equals this Item's id,
then we're going to return that index.
Otherwise we're gonna return zero,
which is just as bogus as it
was the last time we did this.
Okay now, this function
is kinda double bogus.
It's bogus in that we're
still doing this return zero.
In other words, if we can't find the item
in our items, well, we'll
just return the first item,
index zero right there.
That's bogus.
But what's also bogus is that
this code is exactly the same,
yes, as this code over here in our Model.
So, man, you'd never wanna write
the exact same code like this twice.
That is just terrible programming.
Well, we're gonna fix that in a minute.
But first let's just
see if our Grid works.
Believe it or not, this is
all we need for our Grid.
There's nothing else.
So we get the space that's offered to us.
We use this GridLayout to divide it up,
and then we offer it to our
little sub-Views in there,
and then we position
them at those locations
in the GridLayout.
That's all that's required,
so let's take a run here.
See if it worked.
Ah, and it did.
Look at that, there's our six
cards not in a row anymore.
Let's go landscape, and
look, it resized the cards
to fit the space better.
Now, I would like a little bit of padding
between the cards, but
that's a matter back here
of when we create our CardViews,
we can just put a little padding on there.
If we put some padding
around these CardViews
inside of our Grid,
huh, we get the padding.
Huh, works great.
Maybe we don't want quite so much padding.
Maybe we just want padding of five.
Run that.
Huh, that looks pretty darn good.
Okay well, now that our Grid is working,
let's go and fix the bogus
code that we have right here,
which is this index of item.
And it's bogus 'cause
this totally replicates
this other thing.
Let's think a little bit
about what's going on here.
What're we actually asking,
in both cases, to happen?
We have an Array of things
that are Identifiable,
and we have one of
those things, hopefully.
We know it's Identifiable as well.
And we want to look and
see if we can find it.
So, that sounds to be
more like an Array thing.
Let's add a function to
Array that does this.
Extension Array.
But only to Arrays where
the elements in the Array
are Identifiable.
So there it is again,
constrains and gains.
This extension only is
going to add this function
that we wanna add to those kind of Arrays.
And so, it's the same function.
In fact, I'm just gonna copy and paste
and we'll modify this,
or even cut and paste.
Cut that outta there.
Paste this in here.
It's not quite the same in here
because here we were looking at items.
Obviously this is an Array function,
so now items is self.
And same thing here.
It's not the items' count.
It's self's count.
And the type of this item,
index of item, it's not Item.
It's Element 'cause
that's what we're looking
at an Array of.
We have an Array whose
elements are all Identifiable
and what we're saying
here is get the index
of one of the elements of
this Array that's Identifiable
by looking it up.
Now, this is correct and
this does the right thing,
but I actually don't like the
naming that I've chosen here.
One thing about this is
it doesn't actually return
the index of this item.
It returns the first one that it finds.
Going through the Array from zero on up,
and when it finds one, it returns it.
So this is really the
first index of this item.
I also don't like the word item.
Item makes sense up here in Grid
because we have Item.
The word "item" is all over the place.
But really in Array, Item is not a thing.
There's Element, but there's no Item.
And really what this is is the first index
matching that Element.
So I'm gonna use firstIndex
matching as my name.
This feels more like an Array function,
firstIndex matching this Element.
So, how do we use that up here?
Instead of self.index of,
we're just gonna say items, the Array,
dot firstIndex matching the Item.
So we're asking the Array to do this now,
to go find the first
index matching this item.
And we can do exactly
the same thing over here.
We no longer even need this function.
We get it outta here because
we know that our Cards Array
right here can do firstIndex
matching the Card.
So we are reusing this code.
And look how small our Model has become.
It's tiny, it's hardly any code in here.
And some of that is because
we've just made Array
more powerful to look this thing up.
Same thing with Grid.
Grid's very small amount of code
because we're leveraging this Array.
Now, this Array thing has
nothing to do with Grid,
so it does not belong here in Grid.swift.
So I'm going to cut it out Grid.swift.
And I'm gonna put it in its own file.
It's not a SwiftUI View.
It's just a regular Swift file.
Now when we do extensions like this,
we usually call the file Array,
the thing we're extending,
plus some sort of descriptor
of what kind of thing this is.
And so, this is kind of Identifiable,
Array+Identifiable.
So let's go in here.
There it is.
Paste it in.
Notice that this is import Foundation,
which is perfectly fine.
This is not a UI thing.
This is purely Arrays and Identifiables.
None of those are UI things.
So we go back to Grid.
Do build here so it notices
this all this stuff.
And make sure that our Model
also is working, and it is.
No errors, in fact, no
errors in our whole program.
Let's run, make sure that our
Array is doing it, and it is.
All right, so once again, we
are using this whole concept
of constrains and gains.
Here we are constraining the
don't-care type of the Array
so that they're Identifiable,
and that allows us to gain this function.
For all Arrays where elements
are identified they get this.
No Arrays that don't have
Identifiable elements,
they don't even see this function.
Wouldn't even escape complete in Xcode.
It's not even there.
And we're doing the same thing
with constrains and gains here
to make it so that our
Grid is always a Grid
of Identifiable things, matching
two things that are Views.
That is it for this demo.
So I'm gonna go back to the slides,
and we're gonna fix this.
And we need the type in Swift
called Optional to do this,
a very, very important
type, which is an enum.
So let's do a quick
review of what enums are,
and then we'll talk about
this very important type, Optional.
Enums are just another
type like struct or class,
but in the case of an enum,
the value is discrete.
Good example here, FastFoodMenuItem
is either a hamburger
or it's fries or it's a
drink or it's a cookie.
It can't be anything else.
Gotta be one of those four things.
That's the only things on the menu,
and it has to be one and
only one of those things.
Enum, like a struct, is a value type.
Gets copied as you pass it around.
It does not live in the heap.
There's no pointers to it.
It's a value type.
Now what's cool about enums in Swift,
unlike most other languages,
is that each of the discrete values
can have some associated data with it
that's very specific to that
particular discrete value.
For example, we have hamburger here.
And if the FastFoodMenuItem's a hamburger,
we're gonna say how many patties?
Is this a double or a triple or single?
If it's fries, is this a
large fry, small fries?
If it's a drink, you'll see
there, you have this drink,
which is Coke or Dr. Pepper or whatever.
Notice that that String
is unnamed right there,
so these things don't have to have names.
This yellow syntax that you're seeing here
probably, hopefully, looks
familiar from your reading.
This is just a tuple.
So all the rules of tuples
which allow you to have labels
or not labels, and have as
many items as you want here,
all perfectly valid
as the associated data values right here.
So how do you set the value of an enum?
It's very simple.
You just say the name of the enum,
like FastFoodMenuItem,
dot, the discrete value,
hamburger or cookie, like here.
Now of course, in the case
where there's associated data,
you have to provide that associated data.
Hamburger(patties: 2) in this case.
There's no way for it
to create a hamburger
if it doesn't know how
many patties there are.
Now Swift can do type inference here
so that you don't have
to type FastFoodMenuItem
on both sides of this equals.
But you have to be a little bit careful.
You have to put it on
one side of the equals
because in the case if you just say
var yetAnotherItem equals cookie,
Swift does not have enough information
to infer that we're talking
about fast food menu items here.
It could be anything that has a cookie.
So you have to, somewhere,
FastFoodMenuItem has to appear
so that Swift can infer.
How do we check the value?
You might think it would be like an if.
You know, if this menu
item equals a hamburger,
then do something.
But we don't use if with enum.
We use this expression switch.
Now switch exists in other languages.
It's very powerful in Swift,
not only for checking enums
as you're going to see,
but it has regular expression matching
and all kinds of stuff.
Again, you read about that hopefully
in your reading assignment.
So if I wanna check
what value menuItem has,
I'm gonna switch on menuItem,
and then have a case statement
for every single possible menuItem.
And it has to be every single one.
Now, I'm not paying any attention here
to the associated data, and I'm
allowed to ignore that here.
I can still have case
FastFoodMenuItem.hamburger
print the word burger.
I can't say how many patties it is
'cause I haven't looked
at the associated data.
I'm gonna show you that in a second.
But I can still print
burger if I want here.
Swift can infer, of course,
that these are all fast food menu items,
so you don't have to say
case FastFoodMenuItem.hamburger
every time.
You can just switch on the menuItem.
Use .hamburger, .fries, .drink.
It knows menuItem is a
FastFoodMenuItem in this case.
By the way, switch requires you to cover
every single possible case.
So you might have a case, like hamburger,
that you don't care about.
You don't wanna print out hamburger.
In that case, you can just say break.
So, break breaks out of the switch,
and saying case .hamburger
break means it will do nothing
if the menu item is a hamburger.
Similarly, if you really only
care about a couple of cases
and all the rest, you could
just do some default behavior,
you can use the special keyword default.
So a default will happen
if you didn't list
a specific case for something.
But you have to do one or the other.
Either you have to have all the cases
and the ones you don't care
about you break out of,
or you can only have some of the cases,
but then you have to
provide this default case.
It's required in switch in general.
Not just for enums, but always in Swift,
switches require complete
and utter coverage
of all possibilities.
What about that associated data?
Well we can do that in switch as well.
We just add the stuff
you're seeing in yellow here
where when we say case .hamburger,
we say parentheses, let pattyCount.
And that is going to grab the
associated data with hamburger
and assign it to this little
very local variable pattyCount
that's only going to be valid
for this print statement
or whatever that's happening
after this hamburger.
Same thing with the fries.
And for example, the drink is interesting.
So the drink is going to grab
the brand and the ounces.
Notice that ounces used to have
a label when I declared it,
but I didn't do that here and that's okay
because this is tuples.
Tuples you can use the labels
if you want on both sides,
declaring and using it.
And so, this is the exact same
thing that's going on here
where the associated
value, you can grab it
however you want to grab it.
All right, let's see.
Methods, yes, you can
have methods on enums.
It's pretty much unlimited,
whatever you wanna do.
And properties, you can
have computed properties,
but you can't have any stored properties.
All the storage that goes with an enum
is only in these associated values,
which kinda makes sense 'cause
an enum is a discrete thing.
It really wouldn't make
sense to have other data
that applies to all of them.
That wouldn't be discrete.
So, this is why you have to do this.
Switching on self.
All right so, if you have
a function in your enum,
and it's supposed to
essentially tell you something
about the enum, which is really common
to have these kind of functions.
Here I have this function
isIncludedInSpecialOrder number,
which is gonna say whether
this FastFoodMenuItem
is included in a certain special order.
And to do that, I'm gonna
have to switch on myself
and see what I am, to see if I am included
in that special order.
So you can see I have a
bunch of examples here.
One of these examples I wanna
look at a little bit closer,
which is this drink example.
Notice that it's getting the
associated value in the switch
for the ounces, but it's ignoring
whether it's a Coke or Dr. Pepper.
We don't care.
It doesn't matter in terms
of whether it's included
in a special order.
So, this under-bar is the don't
pay any attention to this,
I'm not interested, essentially.
Just like when we have a function,
it has parameters that have external names
and internal names, we
use that under-bar to say
we're not interested in the
external name, don't use it.
That's what this means here as well.
So this is how if you
had some associated value
as a tuple that had multiple things,
you can just ignore some of them
when you're getting the values.
Enums can do constrains
and gains with protocols
just like structs and classes can.
There's a very interesting
protocol called CaseIterable.
CaseIterable, you gain
a var called allCases.
It's static var, so you
send it to the type,
like here, a TeslaModel.allCases
you see there.
And it's just going to return
to you an iterable thing,
something you can do for-in
over of all the cases.
And that can be super valuable.
In your assignment three,
it's likely that you're going
to need to use this.
Now that we know what an enum is,
we can talk about, probably,
the most important enum
in all of Swift, which is called Optional.
So, an Optional looks
essentially like this.
It's got two discrete values.
One is the case none,
and one is the case some.
And in the some case,
it has associated value,
which is a don't-care.
So, Optional, Optional
just works with any type.
It doesn't care what type it is.
So essentially, Optional is either
is an is set case, that's some,
or the not set case, that's the none.
And that's what an Optional is.
Essentially a type.
You're gonna have
variables of type Optional.
They're either gonna be set or not set.
And if they're set, they're
gonna have associated value
of some type, which Optional
doesn't care what type that is.
So where do we use Optional?
Well, you can imagine we use it any time
we have a variable whose value
could either be not set or
unspecified or undetermined,
anything like that.
What're some examples?
Well, that bogus return
type of firstIndex matching.
If the matching thing is not in the Array,
we're currently returning zero.
And that's the first element in the Array.
That's totally wrong.
So instead, we're gonna
have firstIndex matching's
return type be Optional.
It'll be an Optional with the
associated value being an Int.
And so it's either gonna
return set or not set.
It's gonna return set if it was able
and the associated
value will be the index.
Or it's gonna return not set,
the none case right there,
if it couldn't find that
matching thing in there.
Another good example
just of a normal variable
would be, let's say in my
matching game in the Model,
what if I had a variable which is like
the index of the currently face-up Card?
Okay well, when the game starts,
there is no face-up Card.
So what would that index be?
It's like, it's not set.
So we're gonna use an
Optional to store that.
That var is not gonna be
of type Int for the index.
It's gonna be of type Optional.
Now the associated value will be an Int
because of course, if it is set,
we wanna know what that Int is.
There's a couple good
examples which I'm gonna show
in the demo right after this.
So this happens surprisingly often
that you need some variable
that sometimes is not set,
it's unspecified.
You wanna return a value
that says I couldn't do this,
that kinda stuff.
So, Swift introduces a
lot of syntactic sugar,
basically special syntax, to
be able to make it really easy
to use Optional, so much
so that you're gonna think
that Optionals are just some
kind of magic type in Swift.
But underlying it all,
it's just this enum.
There's really nothing
more to it than that.
So let's take a look at
all this syntactic sugar.
First one is how do we declare or to say
that we want this type?
So right here in yellow,
you see that String?.
That is declaring that
hello, this var hello,
is of type Optional,
an Optional whose associated
value is a String.
We would call this an Optional String.
When students are first exposed to this,
they think that the type
of hello is a String
that somehow is modified to be Optional.
No, the type of hello here is an Optional.
Its associated value is a
String, but it's an Optional.
So that's how we declare it.
That is how we type the type,
how we type in with our fingers
the type Optional String.
Now how about setting the value?
Well remember, an Optional
String, it's an Optional.
It only has two cases, none and some.
So we set the none case
by just saying equals nil.
So the keyword nil in
Swift means Optional.none.
That means the not set
case of an Optional is nil.
And similarly, we can say, hello,
this Optional String
equals the String hello.
And Swift is smart enough
to know that that means
set this Optional to the some case
and use hello as the associated value.
So that String, hello
in this case, is set,
and its associated value is hello.
Note that Optionals always start
with an implicit equals nil.
This is nice because remember
that in structs and classes,
all vars have to have an initial value.
We've jumped through a lot of hoops
to try and get all of our vars
to have initial values so far.
Well, Optional, no hoops to jump through
because it gets an implicit equals nil,
or in the enum world,
dot-none, for all vars.
It makes sense, right, because
you've got an Optional here.
It's either set or not set.
Well, it's gonna start out not set.
You could initialize it to
equals hello if you wanted to,
but you can just leave it uninitialized,
but it's not really uninitialized.
It does get initialized to the none case.
So how about accessing the values?
So let's say I have a var like hello,
and it's an Optional,
Optional String let's say,
and I want the String.
How do I get the String?
How do I get that associated value?
Am I gonna have to do, like
you see down here in the right,
switch on it and then do case this?
Of course not.
There's a simple way to
do that switch on it.
Two ways, actually.
One is exclamation point.
If you put an exclamation
point after a var
that is an Optional, it will assume
that it's in the some case, the set case,
and get to you the associated value.
But if you're not, it
crashes your program.
Exclamation point is
truly, well we call it
force unwrapping, because
you're forcing that thing
to be unwrapped and give me that String.
But if that String's not there
because the Optional's
in the not set case,
then it crashes your program.
And we're gonna see in the demo,
this sounds like, ah,
I would never use this.
I would never want my program to crash.
But it can actually be quite useful
in cases where you know
this is never supposed
to be the case.
You can easily find bugs
in development and all that
by making the thing crash and say,
"Wah, that should never have crashed."
But there is a safe way to do it,
and that's by assigning
it to another variable,
a safe variable.
And you do that with if let.
So you see in the lower left corner here,
all the yellow is surrounding
how you do the if let.
You say, if let, some safe version,
equals the Optional, hello is an Optional,
then that safehello is
going to be of type String.
It's going to get the
value if the Optional
is in the some case.
And then inside the
curly braces after there,
if let safehello equals open curly brace,
in that curly brace,
safehello will exist in there,
and it will be a String.
Not an Optional.
It's grabbed the associated
value out of the Optional safely
and is executing.
Now, if that hello were
in the not set case,
then it just wouldn't
even execute that code
that says print(safehello).
Wouldn't even be executed.
It would do the do
something else down there.
That's like, on the right
there, the lower right,
switching on hello.
And in the case that
it's in the not set case,
you're gonna do that
something else that's an else.
Otherwise, if it's in the some case,
then you're just going to
get the String outta there
and do the print(safehello) in there.
So that's how we get the
value, the syntactic sugar
for getting the value of an Optional.
Either forcibly grabbing it out of there
and crashing if we can't find it,
or doing if let to a safe variable,
a safe landing spot for it.
Another cool little syntactic sugar
is the Optional defaulting.
So, this allows you to really
simply provide a default
when you're accessing an Optional
in case that Optional
is in the not set case,
and so it's equal to nil.
Here I have a little constant called x
which is of type Optional
String, Optional String.
And I may have set it to something.
Maybe I set it to nil, maybe
I set it to have something.
But now I'm saying let y equal x.
But if x is nil, use foo.
That's what that question
mark-question mark
means right there.
So, y is always going to be,
in this case, of type String.
Because if x, which is an
Optional, is in the set case
and it has an associated
value, y is gonna get it.
But if x is in the not set case,
then this question mark-question
mark's gonna make y
get the value of foo.
All right, so that's shown on the right
what it would look like
in enum form there.
All right, so Optionals are
best learned about in action,
so we're going to do two major things
with Optionals in this demo.
One, we're gonna fix that
firstIndex of matching
as I mentioned.
And number two, we're gonna make our game,
actually play the Memorize
game, start matching cards.
And to do that, a central piece of that,
is to have a variable that keeps track
of this card that's face-up.
'Cause when there's a card face-up,
I have to match it when
someone picks another card.
So, let's dive right into that demo.
All righty, now that we
know what Optionals are,
we can use it to fix this bogus thing
that we had right here.
What was bogus about this?
Well, we have this function
that found the first index
matching some element in
an Array of Identifiables.
Which it did.
It went through and found the first one.
But if it couldn't find
it, it returned zero.
And zero means the index of
the first thing in the Array,
which is especially bogus
if this Array is empty,
which it could be.
So how're we going to fix this?
We're gonna fix it by
having our return type,
instead of being an Int,
it's going to be an Optional.
Notice I said that this
expression is an Optional.
It's an Optional whose
associated value is an Int,
but it's an Optional.
People sometimes get a little
confused in the beginning
thinking that this is somehow an Int
with some modifier on it.
No, this is a different type from Int.
It's a type called Optional.
This Optional Int, we might call it,
allows us to return nil right here,
or not set value of the Optional,
when we couldn't find it.
And it's really good at
communicating to anyone
who calls this, "I couldn't find this."
Our normal return is returning an Int,
and Swift is smart enough, of course,
that if you have an Optional
Int and you return an Int,
it will return the
Optional in the set state
with the associated value being that Int,
exactly what you want.
So this is it.
Now, this is no longer, in
any way, a bogus function,
it's a completely good function.
And we can use it in the two
places where we call this.
Let's go over here to Grid.
Let's start with this one.
So here is Grid using firstIndex.
It's getting the index.
This local variable right here,
if we option click on it now,
is now of type Optional Int.
Well, that's gonna be a problem down here,
and we'll see an error appear here,
which is "Value of Optional
type Int must be unwrapped
"to a value of Int."
And indeed, layout.location ofItemAt
does not take an Optional as its argument.
It takes an Int.
Now we could take this thing's advice
and say all right, let's unwrap it.
This is how we unwrap an Optional.
This takes an Optional
that's in the set state,
and gets its associated value.
However, when the Optional
is in the not set state,
this crashes your program.
Now some of you might say,
"Whoa, why are we ever
gonna use exclamation point?
"It's terrible, it crashes
my program sometimes."
Well yes, but in this case,
might actually be good
to leave this exclamation point in here.
Because it should never be the case
that we look up the first
index of one of our items,
which we only got by ForEach-ing
through our Item Array.
This should never be nil.
And if it ever were nil,
something is going terribly wrong
somewhere in my code, and
I'd kinda like it to crash
so I find that problem.
But maybe I'm really conservative,
and when I ship my code to my customers,
I really wanna make sure, no matter what,
it doesn't crash with one
of these exclamation points.
So, I could kind of protect my customers
by protecting this code, and say if index
does not equal nil, then do this.
And that will protect this code
so that it can't crash.
Because if index is not nil,
then unwrapping it right here
is always going to work.
But this doesn't build.
And why does this code not build?
It says here "Missing return in a function
"expected to return 'some View'."
Indeed, this function is
supposed to return "some View."
And it does return "some
View" in the case the index
is not nil, but if index is nil,
currently, it returns nothing.
That's a problem.
I'm not even sure what
we would wanna return
in that case here.
So, we're gonna have to take
a little different strategy
to return "some View" here
that has a conditional.
And we've seen it before.
It's over here in our View, ZStack.
ZStack had a conditional.
If the card was face-up, it did one thing.
If it was face-down, it did another thing.
This function right here
that ZStack takes, remember,
is called the ViewBiulder.
It's the same thing that
our GeometryReader takes,
same thing ForEach takes, HStack,
they all take this thing,
ViewBuilder, which is a function,
but it's a special kind of function
where you can put these if-thens in there,
and you can just list Views.
And it turns that all into something
that is "some View."
So the ZStack can have
"some View" as its content.
Well, we can do this same
thing over here in our Grid,
but we don't need to do any layout.
We're doing the layout with
our frame and position.
So we want something
that takes a ViewBuilder
that kinda does nothing.
And there is such a thing.
It's called Group.
So, Group is like a ZStack
or any of these other things
in that its function argument
here is a View builder.
However, it doesn't do anything
to what's inside of here.
It allows you to do the
if and thens and all that,
and you can still list the things,
but it does not lay them out
or try to position them in any way.
And so, our positioning will
continue to work in here.
So, we turn this to a two-line function
instead of a one liner, so
we have to put return in here
to make sure we're returning the Group.
But now we are returning
Group, which is some View.
And Group is just using
the View builder stuff
to go like this.
Now, you might ask, "What does
Group do if index is nil?"
Well, it's gonna return a group
that has some sort of empty content.
Its body is gonna be an empty View.
There's actually a View called EmptyView.
Probably returns that,
but we really don't care.
We're just using the
ViewBuilder functionality here
when we know we can do ifs.
And we're gonna find that Group is useful
in a lot of other situations as well.
As its name implies, it's
good for grouping things.
That's because Group, its View here
is a ViewBuilder View.
That's why we're able to do the if,
but it's also why we would
be able to list some Views,
and thus group them.
So that's really probably the
primary reason Group exists.
That's why it's called Group.
But to be honest, I really
probably wouldn't have
gone through all the
trouble of doing this.
I don't think there's really any reason
to check if this index is nil.
It should never be nil,
as I mentioned before.
So I likely would have
just removed all this.
Gone back to where we were before.
And let this index force unwrap happen.
And if it crashed my
app, it crashed my app.
It never should, and so I
wanna find in development,
and this code is a lot
cleaner without all that
ViewBuilder stuff in there as well.
The other thing I might do is
move this exclamation point
from down here where I'm passing this var
to up here where I'm actually
getting the value of this var.
So, get that firstIndex,
and immediately force unwrap it.
That turns this index variable right here
into an Int, and we can pass it directly.
All right, so there was one other place
where we used firstIndex.
That's in our Model, so let's go over here
and take a look at the damage we wrought
by switching over to firstIndex here.
Here we're gonna use a
little different strategy
to get around the error that
the Optional must be unwrapped
is I'm gonna use the
if in front of my let,
which is a really cool feature.
Just put the if in front,
and then you put curly braces
and do anything you want.
And this anything you want
will only actually happen
if this thing returns non nil.
If it returns nil, this code
just doesn't even get executed.
In here, notice that this
if let allowed us to keep
colon Int as the type of chosenIndex.
We don't need that, by the way,
'cause it's gonna infer it,
but chosenIndex is in fact an Int.
It's only going to actually
exist if this firstIndex
returns a non nil, and
that's the only time
this code's gonna be executed either.
But this does exactly what we want.
It protects us against chosenIndex.
It means choose does nothing.
If we can't find this Card in our cards,
then this does nothing.
That's exactly what we want.
We want it to do nothing.
Let's run and make sure
this Optional business
didn't break anything.
Here we go.
Yup, we can still flip our cards over.
face-up, face-down.
But now, it's time to
try and play the game.
We'll need Optional for that as well.
We're gonna try and make
it so that these cards
will actually match,
and maybe matched cards
will disappear from the game.
That would be cool too.
So how do we do that?
How're we going to play
our game, make it play?
Well, we need a little bit of design.
We are software designers.
We have to think conceptually
how are we gonna make this happen.
So I'm gonna run through,
in my mind, the scenarios.
First, all of my Cards are face-down.
And I click on a Card.
And no matching happens then.
The Card just flips face-up,
which is what it's doing
in our code right there now.
So, okay that's fine.
Second state is I got that one Card up
and I click on a second Card.
Well, that's when I really need to match.
So if there's one Card face-up
and there's a Card clicked on,
that's when I'm gonna play my game.
If there's two Cards face-up
now and I press a third Card,
now I need to essentially turn
those other two Cards face-down.
Whether they're matched or not,
they need to be turned face-down.
And the Card I just touched on,
that's going to be the one that's face-up.
In those three scenarios.
That's all the scenarios there are.
In those three scenarios, the
only time I actually played
a matching game is if there's
one and only one Card face-up
at the time I touch on a new Card.
So I need to detect that case
when there's one and only one.
So I'm gonna have a var
which keeps track of the index
of the one and only face-up Card.
And this is an index into my Array,
so it's gonna be an Int.
But it might be the start of my game
and there's no face-up Card,
or maybe there's two face-up Cards.
So this really needs to be an Optional.
Notice that I didn't
get the error down here
that I haven't initialized this.
There's no errors, but I
haven't initialized this yet.
No errors.
And that's because all Optionals
get initialized to nil automatically.
So there's an equals nil.
You could type it if you want.
But if you don't put it in
there, it's gonna be there.
And that makes sense, right?
Optionals equals nil means it's not set.
Makes total sense that this var
starts out the game not set.
So let's keep going here.
I got this chosenIndex,
and I really only want
to pay attention to Cards that
are obviously in my Array,
but also that are not already face-up.
If a Card is already
face-up and I tap on it,
I'm just gonna ignore it.
Now, you'd think this would work, right?
Get the chosenIndex here,
and the chosenIndex is not face-up.
But it's complaining that
chosenIndex is unresolved.
So if you wanna do an and like this
where you're saying if let of something
and then you wanna do
and on that something,
instead of and here,
you're gonna use comma.
So comma is like a sequential and,
where it does this first.
Then, that's done, it can do
this, and chosenIndex is set.
And you can even have more of these,
like I might also want to ignore Cards
where this shows an
index is not not matched.
So I'm only gonna touch on Cards
that are face-up and unmatched.
Otherwise I just ignore those Cards.
We use that comma notation
quite a bit with these if lets.
Inside here, I know
that I've chosen a Card
that was face-down and not yet matched.
So I just wanna see if
there's one and only one
face-up Card right now, so I'm gonna say
if I can let potentialMatchIndex
equal the indexOfTheOneAndOnlyFaceUpCard,
now I might have a match
because there's one and
only one face-up Card.
I just turned another Card over.
We need to try and match it.
No problem, if the Cards
at the chosenIndex,
if its contents, remember the content
on the Card right here,
that's what's on the Card,
equals the Cards at the
potentialMatchIndex content,
then woohoo, got a match.
Now this is making an error happen.
Why is this making an error?
It says "Binary operator
equals-equals cannot be applied
"to two CardContent operands."
Hmm, indeed both of these
things are type CardContent.
That's our don't-care down here.
Why can't we do
equals-equals on CardContent?
It's a String, right, emoji String.
Op, wait a second.
This is a generic memory
game playing thing.
These are no emoji in there.
They're CardContent, which
is a don't-care for us.
This could be anything, Images,
Strings, Ints, whatever.
We're trying to say equals-equals.
If it's a String, okay, it works fine.
What if it's an Image?
Can you say one Image
equals-equals another?
Maybe not, I don't know.
Equals-equals is not something
that just applies to everything.
And in fact, how does
equals-equals work in Swift?
Equals-equals in Swift, believe
it or not, is not built in
to the language.
It uses a feature in
Swift called operators
that lets you associate
an operator like this
with a function.
And that's exactly what equal-equals does.
It associates it with the
type function equals-equals.
Now, equals-equals might
seem like a funny name
for a function in Swift.
And it is kinda funny, but also remember
that smiley face, the emoji smiley face,
is a valid function name in Swift.
Any Unicode characters,
pretty much, is valid Swift.
So, equals-equals is just
as valid as smiley face.
So this equal-equals
function is a type function,
and it just takes two arguments,
which is the two things on
either side of the equals,
and it returns a bool, whether
they're the same or not.
That's it, that's exactly
what you'd expect.
But not every type has
this equals-equals in it.
Only some types that can actually
check for equality have that.
But luckily, that equals-equals function
is in a protocol called Equatable,
so we can use our
constrains and gains here
to say where our CardContent
implements Equatable.
In other words, we're only gonna work,
our MemoryGame only works
when our CardContent
can be equaled-equaled,
could be Equatable.
Let's go look at this
protocol in the documentation.
So I'm gonna do Option, click.
This is a kind of top-level
documentation about it,
but let's go into the doc.
Equal-equal's a very important protocol.
You can see it's got a lot of explanation
of what it means to say
something is equal-equal
to something else, transitive
property, all this business.
But when you get down to the
list of functions about it,
there's only one that is required.
You see this required?
That means it is part of the protocol,
and there's no default implementation
by an extension anywhere,
so you must implement this.
And it's exactly what I said,
a static, a type function
that takes two of those things,
remember in the slides,
we had is greater than
and you had self, so the
argument is greater than
with an Int or it was a President.
Same thing here.
If you have equals-equals
and if let's a String,
then Self and Self would
be String and String,
so it takes two Strings and compares them.
If it's Int, it takes two Ints, whatever.
This is the only function
you have to implement.
Really easy function, simple
function to understand,
although you have to read
all this to make sure
you really understand it.
And the only one we have
to implement in Equatable.
All these other ones are not required
because you get them
for free by extension.
Swift Foundation gives you these things
for tuples and other
things here automatically.
And does not equals, it
gives you that one for free.
They're all based on
the equals-equals here.
This is a protocol, protocol Equatable.
So we are just forcing, or constraining,
our CardContent, our
don't-care, to be Equatable.
We care a little bit.
We care that it's Equatable
so we can compare it.
All right, so if these two things match,
then I'm just gonna say they're matched.
cards at the chosenIndex,
isMatched equals true.
And of course the cards at
the potentialMatchIndex,
it did turn out to be a match,
so its isMatched also equals true.
So these Cards are matched.
That's awesome.
Notice that no matter what,
whether they match or not,
there are two Cards face-up now.
So the index of the one and
only face-up Card is nil.
There's not one and only one face-up Card.
There are two, so that means this is nil.
What about the else case here?
So in this case, there is not
one and only one face-up Card.
So there's either zero
or there's more than one.
In this case, we wanna turn
all the Cards face-down
except for the one we
just chose of course.
So we're gonna do a little for loop,
index in our cards.indices
And we're just gonna go
for every single Card
isFaceUp equals false, and we'll make sure
that we turn our Card to
true, turn it face-up.
Also notice in this case
that the indexOfTheOneAndOnlyFaceUpCard
is our chosenIndex.
'Cause I just turned
all of these face-down
and I'm gonna turn this one back up,
and so it's going to be
the only face-up Card.
So that's it.
This is our logic.
We don't need this Card chosen
to print anymore right there.
And that's the entirety
of making this work.
So let's run, see if our
game plays like we want.
Well, this game turns out
to be one of the easiest
memory games out there because
all the Cards start face-up.
So, that's no good.
Let's go back to our Model here
and change it so that all
the Cards start face-down.
I think that's gonna make for
a much more challenging game.
All right, let's try it.
Ghost, yeah.
Pumpkin, oh no, no match.
Ghost, yeah, spider, no, no, not again.
Pumpkin, ah, I think I
know where the pumpkin is.
It's right here.
Yes.
And then how about spider?
Oh, now notice that it did turn
my matched cards face-down,
and if I try to touch on them,
I'm currently clicking
on these repeatedly,
they don't turn back up because
they're already matched.
And we ignore cards that
are already matched.
But this is not very good UI
to have these face-down cards
that you can't touch on, it does nothing.
I really wanna take these away.
So if they're matched,
I'm gonna take them away.
So let's go back to our UI and do that.
Here's our CardView.
And here's where the cards
are face-up, no problem there.
Here's where they're face-down.
I'm gonna say if the Card is not matched,
then I'll draw it.
But if it is matched and it's face-down,
I'm gonna draw nothing.
Again, this is ViewBuilder.
It's got these simple ifs.
You can even nest these
ifs inside the elses
or inside the ifs of other ifs.
Nested ifs are okay.
And notice we don't
even need an else here.
ViewBuilder knows how
to deal with the fact
that the if didn't happen, and
so nothing is gonna be here.
Essentially there's an empty View.
And in fact in SwiftUI, there
is a View called EmptyView.
So that's what this
ViewBuilder is going to make
for ZStack in the case
the Cards are face-down.
See if that fixes that.
And we get pumpkin, spider, ghost.
Oh, got a match, ready?
Woo, it took them away.
Another match, takes those away.
Okay, this is excellent.
Our game is basically
functioning beautifully.
So one last thing I wanna
do, though, to my Model.
I'm a little concerned
that I have this state,
the indexOfTheOneAndOnlyFaceUpCard,
that I'm having to keep in sync
with my changes to the Cards.
And this is kind of an
error-prone way to program
when you have state in two places.
The indexOfTheOneAndOnlyFaceUpCard
is here,
and it's also determinable
from these Cards.
So, let's use a computed var here instead
and get from the Cards the
indexOfTheOneAndOnlyFaceUpCard.
And we're also going to do here a set
so that if someone sets the
indexOfTheOneAndOnlyFaceUpCard,
we turn all the other Cards face-down.
This is the first time you've
seen a computed property
that we can get the value of,
but we can also set the value of.
Let's do the set one first.
It's kind of the simpler of the two.
How are we going to react when someone
sets the value of the
indexOfTheOneAndOnlyFaceUpCard?
Well in that case, we need to go through
all of our Cards.
I'm just gonna go
through our indices here.
And I'm gonna pretty much set
all the Cards to be face-down.
Except I don't wanna do that
if this is the one that you said
was the indexOfTheOneAndOnlyFaceUpCard.
Inside this set, there's a
special variable called newValue.
So I can tell you if
index equals newValue,
the value that the person
said this was equal to,
then it's face-up.
So newValue is the special var,
only appears inside this
set for a computed property.
And it's whatever the people set this to.
Could be nil, by the way.
This newValue, it's an
Optional, so it could be nil.
Index is an Int, and Int is never equal to
an Optional that's not set,
so this equals would only be true
if this is an Optional that's set
and its associated integer
matches this integer
So that's it for set, pretty simple.
What about getting?
Well, to get it, I really
need to look at all the Cards
and see which ones are face-up
and see if there's only one.
So let's start by getting
all the face-up Cards.
So I'm gonna say faceUpCardIndices.
It's gonna be an Array of Int.
By the way, I'm gonna use
this syntax right here
to mean Array of Int.
This is exactly the same as Array of Int.
I'm gonna say that out in the real world,
this is actually slightly
preferred as the syntax.
Not 100% sure of that,
but it sure seems to me
people prefer this over Array of Int.
I like Array of Int.
It's clearer that that's a generic,
and the don't-care is Int and all that,
but Arrays are extremely common to use,
so I can understand the shorthand there.
So now, how am I gonna get
this faceUpCardIndices?
It's starting empty right there,
so I'm gonna go for
index in cards.indices,
go through all my Cards.
If a Card at a certain index is face-up,
then I'm gonna put it in
the faceUpCardIndices,
append that index.
All right, now I have all
the face-up Card indices.
I'm gonna say if the
faceUpCardIndices.count equals one,
so one and only one Card,
then I'm gonna return that
faceUpCardIndices sub zero.
By the way, there's another
nice little var for that,
which is .first.
And when we're typing first,
notice that its return type of this var,
or the type of this var,
is Int, question mark,
Optional Int, why?
Because the Array might be empty.
So if you say your Array.first,
and the Array is empty,
this will return nil.
There's Swift using Optionals
to communicate something.
Otherwise, I'm gonna return nil.
Because if there's not exactly one Card
in the face-up Card list, then obviously,
the indexOfTheOneAndOnlyFaceUpCard
is nil, and that's it.
So now that this is calculated,
I don't have to work so hard
to make sure it's kept in sync.
For example, in here, I
don't even need to say
that the
indexOfTheOneAndOnlyFaceUpCard is nil.
Just get rid of that code altogether
because it's always going to know
when there's two face-up Cards.
It calculates it.
Kinda similarly down here, because I set
the one and only face-up
Card to be this index,
I don't need to set all
the rest of them face-down.
That's gonna be automatically
done by the setter of this.
And also, this true of turning it face-up,
we only need to do in here because again,
setting the indexOfTheOneAndOnlyFaceUpCard
is going to make sure that
this chosenIndex is face-up.
So that makes this code
really pretty minimal
to play an entire card-matching game.
Now that's at the expense
of a lot of code up here.
And we're gonna fix that in a second,
but let's make sure that
this has not broken anything.
Here we go.
Pumpkin, spider, spider,
spider, pumpkin, all right.
Ghost, no, pumpkin, ghost,
ghost, ghost, pumpkin.
So this is working.
Beautiful looking code right here.
Little bit more code here than we need.
So how can we reduce this code a bit?
Down here, I can only think of one thing
to make this a little more efficient,
which is that here I'm
saying if something is true,
then set this to true, else set it false.
I could just take this and put it here.
Then I don't need all
the rest of this overhead
of the syntax of all this if.
So I'm just gonna set every Card.
I'm gonna set it to false unless the index
is equal to what the person
said this was equal to,
like I'm doing right here.
So that's nice.
That's pretty minimal code.
What about all this?
Believe it or not, I'm gonna make it
so this is one line of code.
You know we love one line
of code in this class.
Your first homework
assignment proved that.
So how am I gonna make
this one line of code?
I'm gonna start by having
all of this right here
be one line, and I'm gonna do that
with a function in a range called filter.
So I'm gonna take my cards
indices, that's a range
between zero, dot dot,
less than cards.count.
And call this function filter.
You see filter here in range.
It returns an Array of Int.
You see on the left here, Array of Int.
And those Ints are all
the Ints in this range
where this function that takes an Int
and returns a bool returns true.
So it looks like this.
Let's double-click on this.
Here it is.
I can double-click on this even.
It fills it in for me.
This is a function, an in-line function,
that takes an Int and returns a Bool.
And we're supposed to return
whether to include this Int,
which is one of the indices,
in the resulting faceUpCardIndices.
We're making a new Array by
filtering out these other ones.
This is what the index is,
so we'll call this index.
And what do we wanna do?
We wanna say return Cards at that index,
dot, isFaceUp.
So this is all we wanna do.
Of course, we like to make
things less code in Swift,
so we can take away this return.
Maybe we'll put this up here, like this.
We can infer this return type.
Don't need that, don't need
these parentheses in there.
There's another thing I
didn't show you last time
that we can do to make these even smaller,
which is that the arguments
are specified here
with index in.
You can get rid of that
and just use dollar-zero,
and dollar-one, and dollar-two
for each of the arguments.
Dollar-zero for the first argument,
dollar-one for the second
argument, et cetera.
So we're almost there.
This is all gone right here.
And because we now just
calculate this in one fell swoop,
we're getting this warning here
that this can now be a let, which it can.
How about this?
How do we get rid of this?
What's going on here?
Here we're actually just
saying to the Array of indices,
if there's only one of you, give it to me.
Otherwise, give me nil.
Well, why don't we extend Array,
using extension to Array, to
have a var that does this?
'Cause that seems like a
reasonable Array thing.
Hey Mr. Array, give me the
only thing in yourself,
otherwise give me nil.
So let's create another little
extension for Array here.
Do a Swift file.
We'll call this one Array+Only
since it returns the
only thing in an Array.
And this extension to Array
is gonna extend all Arrays.
This extension to Array
is not like this one
where it's where Element's Identifiable.
This one extends to all Arrays.
It's gonna add this var
I'm gonna call only.
It's of type Element, question
mark, Optional Element.
And it's computed, and it
just returns the Array's count
equals one, first, colon, nil.
So this is in Array, so count
is the count of the Array.
And if that equals one,
we're using this ternary operator here
to return the first item in the Array.
Otherwise nil.
Super simple, and that super simple code
makes this all go away right here.
We don't need any of this.
Instead, we can just
say dot-only over here,
and get rid of all of this.
Just return this.
But since this is now a
one-liner, we don't need return.
And we can even make
it all be one line here
like this as well.
And this code has all of a
sudden gotten a lot more compact.
So let's make sure we
didn't break anything there.
Click, yeah, ooh, what, yay, it works.
Now this has set up this
beautifully for your homework.
You're going to now take this working game
and do some enhancements to it.
So check out the writeup for that.
And we'll see you next lecture.
- [Narrator] For more, please
visit us at stanford.edu.
