(light music)
- [Announcer] Stanford University.
- [Instructor] Hello
everybody and welcome,
Stanford CS193p Spring of 2020.
This is lecture three.
I'm gonna start today with a demo,
demo of that reactive SwiftUI
stuff we were talking about.
Then I am gonna jump into some slides,
talk a little bit more
about the Swift type system,
specifically protocols,
and then kind of a totally
different topic after that,
which is the layout system for Views,
how they get laid out on screen.
If time permits,
I'll actually do a little
demo of that layout stuff
at the end of today.
If not, that'll be pushed to next time.
And next time we're definitely going
to lay out our cards in rows and columns
instead of all in a horizontal
line like they are now.
And we'll also get started
on actually having our Model
play the game and implementing
some of our logic.
But for now, let's do
this reactive Swift demo.
You're all really familiar now,
hopefully, with MVVM,
but before I start with this demo,
I'm gonna take just a quick second
to review it just in case,
maybe it's been a few days
since you did your homework.
Here is our Model,
it's called MemoryGame.
It has this don't care CardContent
which is what's on the card,
which as a Model we don't care.
That's up to the UI to decide
what it wants to show on the cards.
Here are our cards.
This is an Array of this
Card struct right down here.
And this choose lets us
essentially play the game.
We haven't implemented any of that yet
but when you choose a card,
that's when the matching
happens and things like that.
And here's how we initialize
or create our MemoryGame.
We obviously wanna specify
how many pairs of cards in our game,
and we need a
cardContentFactory function here
to make the content that's on the card
because for us, it's a don't care.
So somebody who does care
has to make that for us.
And here's our ViewModel.
Our ViewModel has a
var which is the Model.
Our ViewModel can talk
to the Model all at once.
It's a portal or doorway
onto the Model for the View
so it has to be able to do that.
Notice that there are no vars
in here to point to the View.
The ViewModel never talks to its View.
It's the View that talks to the ViewModel.
And we're gonna see how the View
and the ViewModel kind of
Interact in just a moment here,
but there's never gonna be
any connectivity from the
ViewModel to the Views
because many, many different Views
will be using this ViewModel,
this portal, this doorway, onto the Model.
They'll be using it, sharing it.
Again, that's why our
ViewModel is a class.
It's a pointer into something in the heap.
It's easy to share
something that's pointed to.
So our ViewModel also provides
public access to the Model
which is otherwise private.
And that access is both
getting information,
like getting the cards and also letting
the View express its Intent,
in this case, to choose a card.
Both very easily implemented
by us with our Model
but this is what our ViewModels job is.
And finally we have our View.
We have two Views in our
View in the MVVM View sense.
One is a View that
represents a single card
and then this View,
which is all of our cards combined here.
And based on the feedback
that I got from some of you
working on your homework
assignments this week,
I wanna clarify a little bit,
our thinking by clarifying
the words we use to describe things.
A number of you were saying things like,
oh, I'm using this function
to set the font of my card.
And while I guess that's
not exactly not right,
really a better way of saying it is
that this function right here
modifies the View we send it to,
so that it uses this font to draw.
And what's the difference there?
Well, it's the difference
between declarative programming
and imperative programming.
In declarative we're just declaring
that this is the font that
is used to draw this View,
whereas in imperative,
you're thinking things like,
oh, I'm calling this function
to set the font at a
certain moment in time,
I'm setting it.
And there's no moment in
time with this declarative.
At any moment in time,
this should draw the View
that reflects the Model
and it should be able to do
that any time this body is requested.
Boom, it should be a View that reflects
the state of the Model,
essentially time insensitive.
One other thing about this
body is that this var,
you never access this var.
This lower body is never
gonna appear in your code.
This body is called by the system.
Every time the system wants to draw
a View of the Model that this represents,
it will call this var to get that View.
So this is called by the system.
Your job is to implement it
by declaring what the View is,
given the current state of the Model
that you see through this
portal, the ViewModel.
And the last thing I wanna talk about
is some people were
inside these ViewBuilders,
like in ForEach or even
inside ZStack or HStack.
Remember these are ViewBuilders
and ViewBuilders are cool
because they're functions
that return a View
and they do allow this limited if then
and they allow you to just list Views
instead of having to put them
in an Array or something,
you can list them.
People were wanting to put a var here,
var x equals something down here.
And of course you cannot create
vars inside the ViewBuilder.
Vars can not be created
inside these ViewBuilders,
just not allowed to create vars.
So how do you do it?
There's two ways to do it.
One, you can put vars
outside the ViewBuilders,
just in your normal function,
and then I could use x inside here.
But of course if I do that now,
I've created this two line,
I'll have to say return ZStack,
but that's okay, I could do that.
Another way, and probably
a more common way,
is to create another,
let's say this wants to be
an Int, computed property,
and just return whatever
the value you want x to be
and now you can use x in here.
And it's just going to
use this computed property
to calculate the value of x that it wants.
So that's the way we deal
with essentially creating
local variables or variables
that we need to do our drawing in here.
And this is currently called ContentView,
by the way, because
that's where we started
with that template from Xcode.
Xcode didn't know we were
doing a memory game at the time
so it called it ContentView.
Thought before we started,
I would show you how we could rename that
to be a much better name.
Now you might be tempted
to do this in the navigator
with the search and replace feature here.
And you can do that by
going here to replace,
and then you could search, for example,
for ContentView and it would find
all the places it's referenced,
then you could replace it with something
like EmojiMemoryGameView
or something like that.
But actually, this is
not how we would do this.
If we're gonna change the
name of a var or of a type,
we are gonna use refactoring.
So the way we do that
is we're gonna hold down
the Command key, Command + Click.
And when we Command + Click,
we get this nice menu here
where we can jump to
the definition of a type
or a var or something.
We can also rename.
So we do that Command + Click,
and we're gonna go rename.
And it's showing us here all the places
that it found ContentView,
essentially the same thing as
we did the search over here.
Some of them is gonna change,
and you can tell with
this little check mark,
thing's just gonna change.
So it's going to change this file name,
so it's gonna change the
name of that, that's good.
Here it's showing the comment ContentView
but it's not gonna change that.
You see it's not blue, it's gray.
It's just saying, I found this,
but I'm not gonna change that
'cause I'm not sure that
that's really referring
to this type since it's in a comment.
So we'll have to change that ourselves.
It's gonna change down
here this var preViews.
That was actually that code
we scrolled out of the way at
the very beginning that shows
our ContentView in the gray
preview window over here,
so that obviously would
need to be changed.
And then here's the code
in the scene delegate
where we're actually
creating our memory game
and then passing it into
this top level ContentView.
So when you do this
Command + Click and rename,
it actually selects what
you Command + Clicked on
and you can just type something new.
So EmojiMemoryGameView, and see,
it's changing it in all the
other places, not here though.
And that's it.
So you just hit this rename right here
and it has renamed it,
even the name of the file
over here is nicely renamed.
And we can go back and fix the things
that it wasn't sure about,
things like comments right here,
say, this is our
EmojiMemoryGameView.swift which in fact
is the name of this file.
So that was just a
little aside for renaming
'cause sometimes you'll pick a name
for a type or a var
and then you'll decide,
I don't really like that name, it's not,
because naming is really,
really, really important.
Can't overemphasize how important it is
to pick good names for things.
So being able to rename
after you change your mind,
really, really important.
Our topic today though is
going to be this reactive.
I told you that SwiftUI is reactive.
What do I mean by reactive?
I mean that when changes
happen in the Model,
they automatically are going
to show up in the View.
And currently our UI is not reactive.
So that's a problem.
And let's take a look and see
how we can fix that problem
and make it so it is reactive.
We're not gonna implement
our entire memory game here
with all the Card matching.
We're just gonna take a baby step forward
which is we're gonna
make the cards flip over.
That's part of our Model.
Essentially the first
step of our game logic
is to have a card,
when we choose it, flip over.
So I've moved over here to my Model code
where currently we just say card chosen
when you touch on a card,
remember that from last time.
Now I wanna start actually
implementing my game logic
starting with flipping the card over.
Now you'll look at this and you'll think,
oh, this is easy.
Card.isFaceUp equals not
card.isFaceUp, right?
Somebody is passing us the card
they wanna choose as an argument
and I'm just going to have
this face upness of it
be toggled or flipped
to its opposite here.
And this has an error which is that,
Cannot assign to property.
Card is a let constant.
Which it is all arguments to
functions are essentially let.
You don't type that
let, but they are a let.
So that's one problem.
But it's actually much,
much worse than that.
It's not just that this is a let.
This is a Card.
A Card is a struct.
Structs are value types as
we talked about last time.
And a value type, remember,
is copied every time
it's passed as a parameter to a function
or even assigned to another
variable copy, copy, copy.
It's always being copied.
So this Card here is a copy
of one of the Cards here.
The new ViewModel got it
or hand it out to a View
and when the View got it, it was a copy,
maybe even a copy of a copy.
So this is not actually a
Card that's in the Array,
it's a copy of that.
Every time we pass a value
type, it gets copied.
So even if we could do this,
which we can't because of
that problem where this
is all essentially a let,
it wouldn't be doing
the right thing anyway.
This would just be changing
this guy to its space up
to be swapped is having no
effect on this one right here.
So let's try something
totally different here.
Let's try and find out the index
of which Card in this
Array that this card is.
I'm gonna do that by
saying let chosenIndex,
which is gonna be a type Int,
I'm gonna equal to,
I'm gonna call a function on
myself, index of that card.
So index of I'm gonna write a function,
it's gonna be a function
in myself of this card.
So I'm gonna find this card,
find out what index it is in this Array.
So you have func index of
which is gonna be a type Card,
it's gonna return an Int,
and this Int is going to be
the index into this Array.
Now maybe I would say let chosenCard,
which is gonna be a type Card,
equal my cards at that chosenIndex.
It sounds good.
Now I can say now let's
flip the card over,
isFaceUp equals not chosenCard.isFaceUp.
So this seems like this is a good idea,
we'll have to implement this in a minute,
but this seems like this will be great.
Now, we are getting a
Card out of the Array
and we're flipping it over.
But this too is not gonna work
for exactly the same reason.
When we make an assignment
to a var, this chosen Card,
this equals copies this out of here.
It copies the Card out of the Array.
So even just making an assignment
to another variable copies a value type.
So now you're probably like,
well, how are we gonna change this thing?
Well, we have to change it in place.
Instead of changing a
copy of this thing here,
we're going to change this Array itself,
this Array of Cards,
we're going to reach in
there and change whichever
is the right Card to be
the different FaceUp.
So we're gonna do that.
Still going to do self.cards chosenIndex,
but instead of this
indirection through a var
that would copy it,
I'm just going to have that thing directly
is FaceUp equal not that
thing directly isFaceUp.
So I'm just gonna flip the Card over,
directly inside the Array.
A couple of things going on here.
One, notice I'm putting self.in front
of every access to my variables
and to functions I'm calling on myself.
This self.is optional in
the vast majority of cases.
And normally I would even
say, don't put it there.
It's just extra prompt.
However, because of this
thing I talked about last time
where they're going to make it,
so even in the few cases
where you do need a self.,
like here, self.is actually required
in our View when we did our
tap gesture, this self.,
if you take this away,
it's gonna have an error,
you have to fix it, remember that?
So they're gonna take this away.
And when they take that away,
then you're gonna need
self.almost nowhere.
So at that point,
I don't know if that's
two months from now,
it's not clear,
WWDC is at the beginning of June.
A lot of times they'll come out
with new software, beta software,
at least at that point,
maybe they'll fix it then, I don't know.
I don't work at Apple,
I don't know anything
what their plans are,
but we do know that it's
been publicly approved
to get rid of this self..
So in this course,
just for the next seven or
eight weeks left in this course,
I kind of recommend maybe just putting
self.in front of everything
because it doesn't hurt anything
and it'll keep you from
running into this problem
where you have to do the fix it.
You're just learning this stuff
and that's going to kind of
give you the muscle memory
of always typing self.
which you'll have to train yourself out of
in a couple of months when
this public fix comes out.
But it'll keep you out of
trouble in this quarter.
So that's my recommendation
but in your homework,
we're not going to say,
oh, that's wrong, you put self.,
or oh, that's wrong, you didn't put self..
That's totally up to you.
I'm just trying to head off
possible problems for you.
So go for self.is probably
my recommendation.
But (chuckles) we put this in here
and we were sure this was gonna work.
We're for sure changing
isFaceUp and that Array,
there is no doubt
self.cards of chosenIndex.
We are changing this Array to
have it isFaceUp be toggled,
and yet we still have an error here,
Cannot assign to property:
self is immutable.
It's not saying that
this cards is immutable
because the cards is not, it's a var.
If it were a let, it would be immutable,
but it's a var so it's mutable,
but its self itself that is immutable.
In other words, our own function
doesn't seem to be able
to modify our self.
How do we do anything
then if we can't do that?
Well, what's going on here
is that I told you these value types,
Swift is not doing a bitwise copy,
it's not actually copying
them from one place in memory
to another when you pass them around.
That's the semantic,
that's how you have to
imagine it's happening,
but it's really only
actually making those copies
in memory when you start
changing something,
which we are doing here.
We are changing this Array
and this is our actual property,
so we're not making a copy,
it's actually changing the Array,
but this method changes our self.
It modifies our self
because it changes our cards.
So we have to let Swift
know that by saying
that this is a mutating function.
So all functions that modify self
have to be marked mutating in a struct.
This is not true in a class.
Classes are in the heap.
We have pointers to them.
We can always change things
that are in the heap,
always modify things through a pointer,
which, as I talked about before,
can be a bad thing just
as much as a good thing.
But for structs, no,
because they're value types
we have to let Swift
know we're changing this.
Now, index of, which we're
gonna write in a second here,
it's just getting the index
of a Card in this Array.
It's not actually changing anything.
It's just getting the index.
So it does not need or want mutating here.
And similarly or inversely,
our initializer is
implicitly changing our self.
We're creating our self.
We're setting all of our variables here.
So of course this is mutating,
so you don't say mutating in it.
All inits are mutating.
It's all part of the cool
feature of value types
that Swift knows when they're changing.
And you're gonna see that
that has other benefits
other than just this copy
on right behavior as
we copy things around.
Index of Card, how are we going
to find out where this Card
that we're getting the
index of is in this Array?
We want the index into this
Array that has this Card.
This turns out to be super easy for us
because Card is Identifiable.
And since it's Identifiable,
we can just look at its ID
and uniquely see which Card it is.
So we just need to do
a little for loop here
for index in zero dot dot
less than our cards count,
or again, if we're doing self dot,
self.cards.count, either
way is perfectly fine.
I'm just gonna go through every one
and I'm gonna say if
self.cards at that index,
id equals this thing right here,
two equals, by the way,
of I guess, dot id, then I can return
this index 'cause I found it.
Here I'm looking at my Array
to see if the index, if
the Card at that index,
it's id is the same as
this one you passed.
Now, you can already see a
problem here of really of.
we're gonna call this variable of, no.
We don't wanna call this of.
Of is just something that
makes it nice for people
who call us to say self index of card.
That's why we have this dual labeling
of external name, internal name.
And that allows us to
call this card dot id
but callers still get
to say index of card.
This is a great example hopefully,
totally see why we have external names
and internal names of arguments.
We didn't do it here.
The external name and the
internal name are the same.
There's kind of an argument here
you might actually put the
under bar here on choose
because it's clear that
we're choosing a Card
that's the type of this argument,
what else would we choose anyway?
But I'm gonna leave it this way
just so we don't have to
change our other code.
But if you read the guidelines
as part of your homework last week,
you'll see that this is one
where maybe an under bar would be there
and then people would just take choose
and provide the Card is the argument,
there would be no label for the callers.
We've done our for loop, we've found this.
What's still complaining here?
Missing return in a function
expected return Int.
Oh yeah, what if this for
loop goes all the way through
and never finds that Card?
Then we've gotta return something here.
So Swift actually has a fantastic thing
to return here when this happens,
like you go to find
something, you can't find it.
In other languages,
you'd probably return minus
one or some bogus thing here.
I guess I'll return,
maybe you'd return zero
which is the first element
which is totally wrong.
If you go look for the
Card and you can't find it,
you're gonna say, well,
return the first Card
even though you know that's not the Card.
So it doesn't really matter
what we'll return here for now
because this is all just wrong.
But I am gonna put a little thing
called a TODO here, and say bogus.
And a TODO is something
you can go back later,
slash slash TODO,
and find to remind yourself
you need to fix this.
And these show up up here along the top
where it says index up.
If you click,
this is a list of all my vars and funcs,
look bogus appears right there.
And if I click on it, it
takes me right to bogus.
And we will come back when
I show you this cool way
that you can return things
like I couldn't find it from Swift.
We're gonna cover that
probably next lecture
and we'll fix this, we'll fix this.
But for now, we're gonna do
this bogus thing right here.
So we have index of card.
We are definitely flipping
the Card over here.
So let's run and see if this is working.
'cause we know we already have this
hooked up to choose a Card there.
So now it should choose, say card chosen,
and also flip it over, there we go.
Ready, ghost.
Oh pumpkin.
Well it's definitely
accessing the Model here
because we're getting this Card chosen.
But how come these cards
are staying face up?
What is going on here?
Well, what's going on here
is we haven't implemented
the reactive thing.
I told you at the start of this
that's why we're here today,
is to do this reactive thing.
And you can see why we need the reactive.
We chose a card,
it went here and changed the Model,
it flipped a Card over,
and yet we didn't see anything in the UI.
And never should that be
the case in SwiftUI ever.
When you change the Model,
the UI should update.
How do we make that happen?
If you remember from my MVVM slides,
I put some keywords on the screen there
and I said, oh, these Swift key words,
we're going to use those
to do this reactive
and that's exactly what
we're gonna do right now.
And we're gonna start in our ViewModel.
So here's our ViewModel.
And the way we make the
ViewModel participate
in this reactive thing
is using a constrains
and gains thing called ObservableObject.
Now remember constrains and gains,
also known as protocols by the way.
These constrains and gains,
we used one over here, colon View,
that was a constrains and gains.
We were kind of constrained
that we had to implement this body
but we gained all these other functions
that we can send to View.
So this one was a huge gains
for our very small
constrains to do View here.
We also did it in our Model, our card.
We did constraint and
gains to be Identifiable,
which required us to do this var id Int,
but now we gain the ability
to tell which Card is which.
And the UI is going to use that right here
in this ForEach to make sure
that if our cards move around
or whatever, we can track where
they are and animate them.
You're gonna see animation in SwiftUI,
it's ridiculously easy and a lot of it is
because of this mechanism.
So this constrains and gains right here,
the constraints are almost none.
You don't have to implement
any vars or funcs,
no body or id or anything like that.
The tiny little constraint
is that it only works for
classes, ObservableObject.
You can only be an ObservableObject
here if you're a class,
so that's minor constrains there.
Now, the gain you get by doing this
is you get this var
called objectWillChange.
This var right here,
you don't have to put it
here like we did with body,
you get it for free behind the scenes.
So this will not be here.
I'm just showing it to
you, what you're getting.
And this var is not really of this type,
ObservableObjectPublisher,
it's a little more complicated than that.
We don't even know or
care what it is really
but there's two things about
this var that we need to know.
One is that it's a Publisher,
meaning that it can publish to the world,
to anyone who's interested,
and our Views are going to be interested,
when something changes.
And this var can only be sent one function
which is the function send.
And if you call the function
send on objectWillChange,
it's going publish to the world
something changed about this object
or really something will change very soon
so get ready and then react to it.
And that's it, that's all
we have to do, really,
to have our ViewModel participate in this.
So every time our Model changes,
we want to do objectWillChange.send.
For example, here's an Intent.
Clearly we're changing
our Model right here.
We know that choose card
is a mutable mutating function right here.
So of course that's going
to change our Model.
So here we would just say
objectWillChange.send.
This is the only function, really,
we're ever gonna call on
this objectWillChange thing right here.
And this is going to publish to the world,
objectWillChange meaning
this MemoryGame will change.
That's all, it's not saying
how it changed, it changed.
And that's important to know
because if this ViewModel changed,
this portal on the Model has changed,
Views that are looking to
that portal need to redraw themselves.
And we're gonna show you in just a second
how they sign up to do that.
So again, we don't need
to put this in here,
we get it for free.
And when I get rid of that,
you can see no errors.
This is all perfectly legal.
You can call this anytime you want,
anytime something changes.
Doesn't even have to
be your Model changing.
If you were gonna change these emojis,
for example, you could
call objectWillChange,
totally up to you.
However, in a significant app,
you're gonna have,
possibly, a lot of Intents,
a lot of different things
that might change your Model.
And it's a little bit annoying
to have to say objectWillChange.send,
objectWillChange.send.
It's even error prone.
What if I oopsy daisy
forgot to put this in there
and then you choose a Card and it's like,
the cards still don't flip over, why?
Because I forgot to put that in there.
So while we can call
objectWillChange.send anytime we want,
usually the way we deal with this
is we take this var and
we make it Published.
So this is not a Swift keyword,
you can see it's not magenta,
it's something called a property wrapper,
this is a property.
And property wrappers add
a little functionality
around a property.
In this case what this wrapper does
is every time this property,
this Model, changes,
it calls objectWillChange.send.
That's what it does.
So really, to make it so that
our ObservableObject here,
our ViewModel, broadcasts
every time something changes,
we just need to @Published all of our vars
that we care whether they
change when in this case
it's just our one Model var.
It could be other things as well.
You can have as many of these
@Published vars as you want.
Any time any of them change,
it's going to objectWillChange.send,
that's what it does.
This looks all a lot nicer
than having to remember
to put objectWillChange.send
in all these functions.
You still have the option of
doing objectWillChange.send
even if you use these,
but most of the time you will not have
to do objectWillChange.send.
So we're almost there.
Right now we've got our ViewModel
so it's publishing every
time the Model changes.
Now we just need to fix our View over here
so that when it sees this
ViewModel publishing, it redraws.
Redraws every time it sees this thing,
say objectWillChange.send.
And the way it does that is
with another property wrapper
on this one called @ObservedObject.
And that's saying this var
has an ObservableObject in it,
which it does, EmojiMemoryGame
is an ObservableObject.
And every time it says
objectWillChange.send, redraw.
And of course redrawing this
one is gonna cause this one
potentially to get redrawn as well.
Now, you might kind of feel like,
whoa, this could be really inefficient,
what if the Model is
changing all the time,
are we gonna be redrawing
our Views every single time?
Well, yes and no.
Yes, we're going to be
reacting to objectWillChange
and redrawing, but SwiftUI
is smart about seeing
whether something actually changed.
So if we flip one card over,
it's not going to redraw
every single card,
just the one that changed and it knows
because the Cards are Identifiable.
You starting to see why this
ForEach on this Array of Cards
forced us to make this Identifiable.
It helps it to understand,
oh, this one changed
so I actually need to redraw that.
Because actually calling this code
is probably not very expensive.
Actually drawing on screen,
that's pretty expensive.
So SwiftUI does everything
it can to avoid doing that.
But conceptually for us, it's so simple.
Every time our Model changes,
this @Published wrapper notices that,
we're an ObservableObject,
so our View can mark that it's
interested in that publishing
and voila, it's gonna redraw this.
And this is how we do
reactive programming.
Really, that's all there is to it.
We're gonna see some minor refinements
to that down the road,
but this is essentially how it works.
So let's see if it works.
Let's go run our app.
Hopefully when we click on the cards,
we're gonna say card chosen
but they're also going to flip over.
So let's try the ghost.
The ghost, I'm clicking on
it, it's flipping both ways.
How 'bout this guy and
this guy and this guy.
All right.
So what's happening here is
we are clicking on these,
it's doing this onTapGesture.
It's expressing this
Intent in the ViewModel.
The ViewModel is then asking the Model
to go ahead and do it.
The Model is doing this mutating thing.
Once it's done this mutating thing,
it's easy for this to notice
that this has changed
and it gets published,
objectWillChange.send,
and this guy observes
those objectWillChange.send
and is redrawing.
That is the slides that I showed you,
the back and forth, that's
what it looks like in code.
So we need to jump back
into the slides right now
and talk a little bit about protocols.
A protocol is gonna look to you
like it's a stripped-down class or struct,
stripped down because it
has functions and vars
but no implementation.
So here's a protocol,
moveable, that I've made up.
It has one function and two vars.
One of the vars is read
only there hasMoved.
You can see it has that
curly brace get there.
And then distanceFromStart
is actually readable
and writeable, that's
why it has get and set.
But there's no implementation here.
Even those curly braces there,
that's just saying whether
those vars are read only
or not, that's all there is.
Once you have a protocol declared,
now any type, struct or class,
can come along and say,
yes, I'm gonna implement that.
That's claiming to implement the protocol.
So here I have a struct,
portable thing and it says colon Moveable
in this declaration.
And when it says that,
that immediately means I
sign up to implement this
and therefore it must implement every var
and every function in Moveable.
Now we've seen this before View,
we have our ContentView, colon View.
It signs up to be a View and that's why
it has to do var body,
same thing Card was, Identifiable.
It signed up to implement Identifiable.
It had to implement that var id.
Now it's also possible
to have one protocol say
that it requires another protocol.
This is called protocol inheritance,
don't get confused with class inheritance
'cause we're just talking
about protocols here.
So here I have a protocol Vehicle
and it's inheriting from Moveable.
It adds its own var there, passengerCount.
So if a class like car comes
along at the bottom here
and it says, I signed up to your vehicle,
well now it has to implement
all three things from Moveable
and it has to implement
the thing from Vehicle.
You can also, if you're
a struct or a class,
claim to implement multiple protocols.
So here I have the class car.
It's not only saying that it's a vehicle
but also that it's
Impoundable and Leaseable
and now cars can have to
implement all the functions
in vars in all three of these protocols.
Now a protocol is a type.
That means that most protocols can be used
in most circumstances
where you have a type.
For example, I can have a
variable m of type Moveable.
That's the type of m,
it's a type Moveable.
And what does that mean?
Well, if I had another couple of vars,
like car and portable,
which are of type Car
and type PortableThing,
then I can say, m equals
car or m equals portable.
Why can I say that?
Because car is a Moveable,
it implements the Moveable protocol.
In fact, car implements Vehicle,
Vehicle inherits Moveable,
and so therefore car is Moveable.
And this is great 'cause
now I have this variable m,
I can start sending it
functions like has moved,
has moved because I know
that m is a Moveable.
And whether it's a Car in
there or PortableThing,
we know that those vars and functions
are gonna be implemented
because you're required to implement them
if you say you're one of those things.
But one thing to be a
little careful of here,
you cannot say portable equals car.
The var portable up there
is not of type Moveable,
it's of type PortableThing, different.
And so a Car is not a PortableThing.
They're both Moveables,
but a Car is a different
type than a PortableThing.
I think a Car was a class,
PortableThing was a struct.
So not even the same kind of thing.
So you cannot say that.
While I can say m equals
each of those things,
I can't say they equal each other
'cause Swift is enforcing
the type of the var,
and then when I say portable equals,
the type of the var is PortableThing,
not type Moveable there.
One way to think about protocols,
and I've already mentioned
this in the demo,
is constrains and gains.
I use this because it rhymes,
so hopefully easy to remember,
and it works like this.
So I have this struct right here,
Tesla, and it's a Vehicle,
so it implements all of those things.
In fact, it's constrained
to implement all the things
in Vehicle which includes
all the things in Moveable,
but being constrained on that
is going to make it gain,
all the things the world
offers to a Vehicle.
Now, you might be saying,
well, wait a second here,
Vehicle is a protocol,
has no implementation.
How are we possibly
gonna gain anything here?
It seems like I got all constrains
here and I got no gains.
Well, the magic is in
the keyword extension.
In Swift, we can extend
protocols to have implementation.
and we just say extension,
name of the protocol,
and then we can put functions
with implementation,
functions in vars with implementation.
Now we can't have any vars
that have storage here.
So there is that restriction,
it has to be computed vars,
like var body was computed,
remember, it had the curly braces,
have to do the same thing here,
but we can add as many things as we want.
With this extension of
Vehicle registerWithDMV,
now Teslas and all other Vehicles
can be registered with DMV.
In other words, they gained that ability
by living with the
constraint that they have had
to implement those methods and vars
that were in those protocols.
So, yeah, this is really the center
of functional programming in Swift.
And the protocol View is probably
the poster child for doing this.
And we're gonna see more about View
in a couple of slides here.
In addition to adding functions,
like registerWithDMV, you
can also use an extension
to protocol if you want to
add default implementations.
So here I am extending Moveable
and I'm actually providing
a default implementation
for hasMoved as hasMoved
is one of the vars in the protocol
but I'm providing a default
implementation here.
I'm just looking at my
distance from start,
and if that's greater than zero,
I'm gonna assume I've moved.
And I'm doing this with
an extension to Moveable.
So this makes it possible
for me then to have a struct,
I have one here called
ChessPiece which is a Moveable,
you can move chess pieces.
ChessPiece does not need
to implement hasMoved.
If ChessPiece just implements
moveBy and distanceFromStart,
then it will have successfully
implemented Moveable
because it'll pick up the
default implementation
from that extension right there.
Now, if ChessPiece wanted to
implement hasMoved itself,
it could, but it doesn't have to
because there's a default implementation
for hasMoved in that extension.
Now, you can use extensions of course,
to add code to structs
and classes as well,
not just protocols.
So here, for example, I
have a struct called Boat
and it's got its own methods,
whatever they might be.
And here I'm adding extension to Boat,
a function sailAroundTheWorld.
And this extension, you
can see, has curly braces,
it has an implementation.
This is an actual implementation
of sailAroundTheWorld
that we've added to Boat.
You can even make something
like a Boat conformed
to a protocol purely by
using your extension.
So Boat doesn't implement
any protocols right now
but I can make Boat implement Moveable
by having the extension to
Boat that says colon Moveable
and then in that extension,
implement moveBy and distanceFromStart.
Now Boat is a Moveable.
And I added it totally with extension.
It's not an uncommon thing to do
to take a structure or
class and make it conformed
to a protocol using purely an extension,
or you add the code in an extension.
Why do we do all this protocol stuff?
Now, for those of you who are coming
from object oriented programming,
this was gonna seem like,
what's going on here?
Why do we do this?
Well, there's a really
good conceptual reason
why we're doing this.
Protocols are a way for
types, structs and classes,
other protocols, even enums,
which we haven't talked about,
to say what they are capable of,
what functions they can do,
what vars they have on them,
and it's also a way for other code
to demand certain behavior
from other objects
by demanding that they
conform to a protocol,
either by having a variable of that type
that they're trying to assign
or parameter to a function.
And there's even other
mechanisms you're gonna see
soon when we talk about
generics and protocols
for demanding that you want
that thing to be a Moveable.
It has to be a Vehicle, whatever.
You can demand it now that
you have this protocol.
But in all of that,
neither side has to reveal
what sort of structure class you are.
You completely can be anything you want.
You just say you implement Moveable
and now you can be
operated on as a Moveable,
but you could be anything.
You could be a Car,
you could be a PortableThing.
You could be a Boat.
We know when neither side cares.
All one side cares is that
you can do the Moveable things
and all the other side cares is that it
implement all those Moveable things.
So this is what functional programming,
or really we might call it,
protocol-oriented
programming, is all about.
It's about formalizing how data structures
in our application
function, how they behaved.
Even when we talk about vars
in the context of protocols,
we don't define how
they're stored or computed,
we don't even say where
they're stored or computed,
we just talk about whether
they're read only or read, write.
And through all this we
focus on the functionality.
We're hiding the implementation details.
It's kind of the ultimate
promise of encapsulation
from object-oriented programming
but it's really taken to a higher level
because it doesn't mix it inexorably
with the data and all that,
it's just talking about the functionality.
And all of this gets even more powerful
when we combine it with generics.
Protocols plus generics equals,
as I say here, super powers.
So let's look at how generics,
remember that's the don't care stuff
that we talked about last time,
how it combines with protocols
to make super powers.
Here we go.
Let's do this by example.
Let's say I had a
protocol called Greatness,
and this protocol only
has one function in it
which is, isGreaterThan other.
One argument other to this
function isGreaterThan.
By the way, this is kind
of an interesting function
because the type of
other is capital S Self.
That's a special kind of
name of a type in a protocol
which means the actual type
that's implementing this protocol,
because remember, protocols themselves
have no implementation,
they get implemented
by structs and classes.
So that Self means that
the actual structure class
that implements this,
that is executing it at the time.
So that's kind of cool.
And I'm gonna show you how that works
in just a few clicks here.
So if we have this protocol,
look at what we can do.
extension Array, so I'm
adding something to Array,
where the Element,
the don't care of the Array,
conforms to Greatness, colon Greatness.
So that where I put in red
because it's really the key part
of connecting generics and protocols.
Here, I'm actually going to add a var,
or I could do it with functions,
but I'm gonna add a var here to Array
so that every Array where
the Element, the don't care,
conforms to Greatness will get this var.
Let that sink in.
Now, this var will not exist in Arrays
where the don't cares don't implement
the protocol Greatness.
This var just will not be there.
If you tried to type it in your code,
the compiler would say,
oh, this Array does
not implement greatest.
It would only say it implements greatest,
only lets you type that in without error
if it was an Array of
something that implements
the protocol Greatness.
Now I like to call this,
we care a little bit.
(chuckles) Right?
Normally we call generics don't care,
Element is a don't care,
Array doesn't care what's inside of it.
Well, this is kind of,
this extension to Array cares
a little bit about Element.
It doesn't really care what Element is,
can be any struct or class,
but we do care that it
implements Greatness.
So this is care a little bit,
you wanna think of it that way.
Then this var greatness,
look what its type is, Element.
It's the don't care 'cause I'm
gonna look through the Array
and find the one that's
the greatest by calling
isGreaterThan other on all of the things
in the Array which I know I can do
because this extension to Array
is only where the Elements
implement Greatness.
See how it all works here?
So you can easily imagine,
I'm not gonna show the code here,
but you can easily imagine
building a for loop
that just goes through all the Elements,
calls isGreaterThan on all of them,
figures out which one is the greatest
and then just returns it.
Some of you are looking at all this,
and I'm sure you're
shivering, as I say here.
You gotta be thinking, holy cow,
how am I supposed to be expected to know
how to design my code
using this technology?
I mean, this is just all new to me.
And this is indeed a
very powerful foundation
for designing things, very powerful.
But functional programming does require
some mastery that only
comes with experience.
And the good news is that you can do
a lot of stuff in SwiftUI, most things,
without really mastering
functional programming.
But here you are at Stanford
trying to get a good education.
And so the reason I'm
explaining this to you now
is so that the more you use it in SwiftUI,
the more you see it in the documentation
where you see these wares happening,
we see protocols like View
and Identifiable coming down the road,
that you're not just saying,
I have to put colon View here,
you're actually understanding
how it's being designed underneath.
And the more you see it,
the more it's gonna sink in,
and the more eventually you
might start to be capable
of doing functional
programming design as well.
But no one expects you, right now,
to be able to be designing stuff
where you're adding extensions
to protocols with generics
and all that.
But eventually you'll be able to.
And in the meantime you'll kind of know
what's going on in SwiftUI.
So I'm just putting this one
slide up here about enum,
but once again, not gonna talk
about enum in this lecture.
We'll talk about it soon, not to worry.
So that pretty much covers
what we're going to talk
about today on architecture
and we're almost there in
covering this entire topic.
And now I'm gonna shift gears entirely
and go to a completely different topic
which is layout, in other words,
how do we decide where all
our Views go on screen?
The way that SwiftUI does
this is amazingly simple.
It's one of the more elegant
things in all of SwiftUI.
There's really only three
steps to doing this.
The first one is the Container Views,
like HStacks and VStacks
and things like that.
They offer space to the Views
that are inside of them.
And then those Views choose
a size for themselves,
what they want to be.
Based on that offer,
they could choose a
size same as the offer,
that's the most common,
they can choose a size
smaller than the offer,
they could choose a size that's
larger even than the offer.
So they use, in a very kind
of good encapsulation way,
Views decide what size
they're going to be.
No one tells them what size to be.
We just offer them space, they decide.
But then after that, the
Container Views like the stacks,
it's their job to position their
Views inside of themselves.
And that's it, these are the three steps
to get everything laid out in SwiftUI.
So let's dive into this a little bit.
Let's talk about Container Views.
So the most common Container View
that you're already familiar
with is HStack and VStack.
Of course, there's ZStack as well
that kind of stacks them
on top of each other,
but HStack and VStack are interesting
in that they divide up the
space that's offered to them
amongst all of their subviews.
And we'll talk about how
that works in a moment.
ForEach is kind of an
unusual Container View.
It actually defers the positioning
and sizing to the container that it's in,
that's why we put our
CardViews that were in
that ForEach into an HStack.
So their ForEach is deferring
letting the HStack decide.
And a hidden thing that's going
on with layout is modifiers,
like .padding and others.
They essentially contain
the View that they modify,
if you wanna think of it that way,
and some of them, like padding, do layout.
So let's talk a little bit more
in detail about HStack and VStack,
the most important one or at least the one
that's doing the most layout.
The way that the stacks
divide up the space
that they're offered is
kind of divided equally
and then they offer it to the
least flexible Views first.
So what do we mean by that?
So an example of a very space
inflexible View is Image.
So we haven't talked about image yet
but it's just a View that shows an image,
as you might imagine.
And of course it wants to
be the size of that image,
pretty inflexible in that way.
So generally the Images
are gonna get the space they want first.
Another example of a
pretty inflexible View,
not quite as inflexible as Image is Text.
Text always wants to size itself
to fit the text inside
of it, understandably,
but it does have a way to
be a little bit smaller
and put dot dot dot at the end of the text
as we'll talk about in a second here.
So it's not quite as
inflexible as an Image.
And most Views are very flexible.
For example, all the Shapes,
like RoundedRectangle that we saw,
whatever size you offer it,
it's pretty much gonna take that
and it's going to draw itself
appropriately in that size.
So after one of these
Views chooses its own size
and takes whatever size it wants,
that size is removed from
the space that the stack
is trying to allocate,
and then it goes on to the
next least flexible Views
and rinse and repeat until
all the space is used up.
So it's as simple as that.
That's how HStack and VStack
apportion their space.
Now, after all the Views
have chosen their size then the HStack
and VStack sizes itself to fit those Views
with whatever little spacing
in between that it provides.
HStack and VStack work with any View,
of course, but there's a couple of them
that I'm gonna introduce to you here
that really help with layout.
One of them is called Spacer.
So a Spacer is just a View
that takes all of the
space that's offered to it.
So if you give it space,
it's gonna use that space.
And so it's used for filling in space.
Now it doesn't actually draw anything,
it just kind of uses up space in an HStack
or VStack, that's why
it's called a Spacer.
It has that minLength argument
which is the minimum size
that it should be in the direction
we're laying out
horizontally or vertically,
depending on whether
it's HStack or VStack,
although we usually don't specify
because the default for
that is the right amount
of space on this platform.
One thing that you're gonna start
to get used to as the quarter goes on,
that even though we're focusing on iOS,
you can use SwiftUI on
Apple Watch and Apple TV
and the spacing and the layout
is a little different on these platforms.
And SwiftUI is really smart about saying,
I'm on an Apple watch so I'm
gonna use this much space
in my Spacer by default or whatever.
So that's why we really wanna try
and use these default,
and not specify minLength
when we use Spacer.
Same thing with spacing
on the HStack itself,
HStack spacing, we don't wanna do that.
Even padding, generally
it's fine to put padding
but if you start putting numbers in there
and specifying exact paddings like we saw,
you're defeating a little bit
of the purpose of this
platform independence.
Now, sometimes you need it,
you just do but we try
to use these defaults
as much as possible.
So another cool View to put in
an HStack or VStack is Divider.
So Divider just draws a dividing line,
again, platform specific,
it depends what a divider looks like
in the context that it's in.
Of course, the Divider
is not like a Spacer,
it doesn't use all up all that space.
It only uses enough
space to draw that line.
And the line obviously goes
opposite to the direction
that we're laying out.
So for an HStack,
the divider is obviously
gonna be a vertical line,
and for a VStack, it's
gonna be a horizontal line.
So you're almost certainly
gonna want to use one
or both of these in your
next homework assignment.
And they're really valuable
for doing layout with stacks.
These HStacks and VStacks,
I told you that they're kind of choosing
which of its Views to offer
space to next priority wise
using this least flexible thing,
but you can actually override that
with this View modifier layoutPriority.
So here's an example of an HStack
that has a Text that's really important.
It's got an Image which we
know is very not flexible,
and so normally would get a
lot of attention from HStack.
And then it has another Text
which is less important.
So I've added the View
modifier dot layoutPriority 100
which I can pick any
number I want there really,
it doesn't matter, it's
a floating point number.
And that's more than the
default layoutPriority
which is zero.
So when this HStack
goes to offer its space,
it's going to offer this Text space first.
And that Text is gonna say,
well, sure, I wanna be this big
so I can fit this word,
important, all the way.
Then it's gonna say, okay,
well there's no other high priority ones.
There could be other ones
with different numbers.
It starts with the highest
priority and goes down.
And so now it goes to do the Image
because that's less
flexible than the Text,
Image gets it space and
then the unimportant text
has to fit itself into
the space remaining.
And as we mentioned before,
when a Text doesn't get enough space,
it will put dot dot dot in there to elide
or shorten the text to
fit the space it did get.
It always wants to be its space.
It never wants to be larger
than its text fits in.
It always wants to be the exact size
but if it's forced to be smaller,
it knows how to do dot dot dot.
Another and significant part
of HStack and VStack's layout
is their alignment.
So imagine you have a VStack,
a vertical stack of Views.
And what if those Views
pick their own size
and they're not the same width.
So they can't all be kind of filling
the whole width of this vertical stack.
So does the VStack left align them
or center them or right align them?
How does it know where to put them?
Well, there's actually an
argument to VStack and HStack.
We already know that VStack and HStack
have the argument spacing which determines
the spacing between the Views.
It also has another argument, alignment.
And alignment takes an
alignment guide as its argument.
And one of the alignment
guides, for example, is leading.
Now, why leading here instead of left?
If I just want this Vstack
to have all of these be left aligned,
why don't I say dot left?
And in fact, there's no
such thing as dot left.
And .leading means to have the things
in the VStack line up
so that their edges start
from where text comes from.
In different languages sometimes
the text comes from the
right and moves to the left,
like Hebrew and Arabic.
So we want our VStacks
to generally match up
with that text coming from that side.
Text baselines can also be lined up.
So HStacks, well, it only
makes sense in an HStack.
You can line it up so that
the bottom of the text,
even if they're different fonts,
will all be lined up in your HStack.
You can even define your own things
to line up alignment guides.
And that's a little beyond
the scope of this course.
So we're just gonna use the built in ones,
like .center which is
usually the default alignment
is to center the thing in the middle
of the VStack or the HStack.
But there's also top and bottom trailing
leading all these things.
As you can imagine, when you
just start typing in an Xcode,
VStack alignment colon, of
course, Xcode will help you
and tell you what all those built-ins are.
That's it for stacks.
Stacks are very important.
But there's this other thing,
modifiers, like .padding,
that I said kind of act like
essentially Container Views.
Remember that these things,
.padding, et cetera, they return a View.
And you might've thought
they just return a View
so that we could then send them another,
call another function on them,
.foregroundColor and then
that gives us a View back,
we call .padding on that.
And that gives us a View
back and call.font on that.
So it's not just to give us a View back
so we can call another function on it.
Those Views that come back
might actually be
participating in the layout.
Now, most of them don't participate,
like font and foregroundColor.
They're not affecting layout
so any space they're offered by an Hstack
or some other container,
they're just gonna pass it on
to whatever View they contain.
For the purposes of this slide,
we're gonna think of a View
that these modifiers modify as
being contained by that View
that the modifier returns
which it kind of is.
We're gonna see how these View modifiers
are made next week,
probably, or the week after,
depending on how things go.
And you'll see that kind
of really what's happening
is that it's containing
the View it's modifying.
What about these modifiers
that actually participate
in the layout process like padding.
Let's look at padding.
The View that's returned
by .padding, this modifier,
it offers the View that
it's modifying a space
that's the same size as was offered to it
but reduced by 10 points in this case,
whatever the padding is.
It might be if that 10 is not there,
then it would be whatever
system appropriate padding.
It's essentially removing that 10
because it knows it's
supposed to provide the 10.
And then the View that's
returned by padding
chooses its own size to be whatever size
the thing that it's modifying
ended up being plus 10.
So that's what padding does,
it adds 10 points around the outside
or whatever edges you say.
Another thing is modifying.
So you see how .padding
modifier is just a View
that participates in the layout.
What's another example of this?
You've already used it in
your homework, .aspectRatio.
The View that's returned
by .aspectRatio modifier,
it takes the space offered to it
and it picks a size for itself
that's either smaller
than that offered size
and has the aspect ratio,
that's if we choose the
.fit option of aspectRatio,
or it could be bigger than
the size that's offered to it,
that's the .fill option that
uses all the offered space.
And so yes, it is possible
when you're a View,
when you're offered a
certain amount of space,
you can choose your size to be larger.
Now that's rare.
We don't generally want Views kind of
spilling out all over each other,
but it makes sense,
like in this aspectRatio
fill, maybe that makes sense.
So then the aspectRatio
View that's now sized itself
to have that aspect ratio,
offers the space that it chose
to whatever View it's
modifying, like our CardView.
So our CardView fits itself then
in that nice aspect ratio space.
So let's see an example
of a full layout happening
in the size being passed
around and things like that.
So here's an HStack similar to the one
we have in our Memorize game,
not exactly the same
but very, very similar.
How is the space for
this thing apportioned?
Well, the first thing to
understand is that the first View
that's gonna be offered the space into
which this whole green thing
goes is that padding View.
It's the outermost View.
It's actually the View that
is this whole thing in the end
but it's gonna be offered whatever space
is available for this
whole green construct here.
Now what it's going to
do, it's going to reduce,
take 10 points off the edges
of it and pass that space
that's left onto the next View
which is the View returned by
the foregroundColor modifier.
Now that modifier isn't really
participating in the layout,
doesn't really have any effect
on the size and position the thing,
so it just passes that on
untouched to the HStack.
Now, the HStack, as we know,
is big time layout View.
It's going to divide up its
space starting out equally
and since the aspectRatios
aren't things like Images
or Texts that are fixed sizes,
it's going to end up
dividing the space equally
among all of the aspectRatio
Views in the ForEach,
because we know the ForEach itself
just defers to the HStack.
So it's the aspectRatio Views now
that are being laid out in HStack.
Each aspectRatio View is
going to set its width
to be its share of the HStack's width
and then pick a height that
matches the aspect ratio,
the 2/3 aspect ratio,
or if the height is limited here,
it might be the other way around
where the aspectRatio View takes
all of the height it's offered
and instead chooses a
width that's less to fit.
So it could go either way.
It depends on whichever is gonna fit best
in the space that is offered.
Then the aspectRatio has picked that size.
It's going to offer that to the CardView
and the CardView is going
to use all of the space
because it's like a normal View,
whatever space you offer it,
pretty much it's going to use.
After all this offering
and sizing happens,
what's going to be the size
of this whole green View?
Well, it's gonna be the size
of whatever View.padding 10
returns which is the result
of the HStack sizing itself to fit
all those aspectRatio Views
plus 10 points on all sides.
That's gonna be the size
of this whole green thing.
Let's talk about Views that take
all the space that's offered to them.
Obviously things like RoundedRectangle,
it's real easy for them.
They just crawl around
a rectangle all the way
to the edges of what they're offered.
But what about custom Views like CardView?
CardView we built out of a ZStack
with RoundedRectangle, Texts,
we're building this thing.
It takes all the space
that's offered to it,
and there's no reason it shouldn't,
but it should really be adapting itself
to whatever space was offered.
And we really see this desperately
with the font size of the emoji.
And in your homework I asked
you for really small cards
to switch to a smaller font.
And I'm sure you'll probably realize,
wow, this is a really bad
solution to this problem.
And of course it is.
Really, what we need to do and
we're gonna do in our demo,
is pick a font size that
is related to the size that were offered.
Since we're gonna accept
the size offered to us,
we should pick this
font size that fits it.
So how does a View know
what space was offered to it
and can make that font
choosing decision, for example?
Well, we do that with a special View.
It's just a View,
but it is kind of special
called GeometryReader.
And what you do with a GeometryReader View
is it wraps around whatever thing
that you want to adapt to the size.
And so this would normally just
take whatever's in your body
and you just wrap GeometryReader
around it like this.
GeometryReader is just a View.
And I'm not showing you
the obvious thing here.
GeometryReader, open
parentheses, content, colon,
all this, just like in HStack or whatever,
this is just the content.
But you do notice that it has
a little argument there geometry in,
similar to how ForEach has an argument
which is the thing we're iterating over.
This also has an argument.
So this argument is of type GeometryProxy.
And this GeometryProxy is just a struct
and it has some nice information in there,
the most important of which
is the first one you see,
var size, that is the
size you're being offered.
The width and height CGSize
is a width and height
that you're being offered.
And you can use that size, it's in points,
and of course font sizes are in points.
So it's gonna be pretty easy
for us to pick a font size
that fits nicely in that size.
You see some other things here,
like the frame is
actually not only the size
but it's a rectangle where we are in
a certain coordinate space like
our parents coordinate space
or the global coordinate space,
we can even look at if we like.
And I'm gonna talk about
the last one there,
safeAreaInsets, on the next slide.
One thing to remember
about GeometeryReader,
it's just a View but it always accepts
the space offered to it.
And I underline that because
it requires a little sinking in
'cause you don't wanna get
into a recursive loop here
where the GeometryReader
is reading its size
and then you're trying to
actually change the size
of the GeometryReader based
on the size that it read.
It doesn't work that way.
GeometryReaders always accept the size,
the space, that's offered to them,
you have to think of it that way.
So GeometryReaders utility is just limited
to knowing what size you're being offered
and adjusting how you look on the inside,
that's what GeometryReader is for.
Don't try to twist GeometryReader
into something it's not.
It's just reading your geometry,
it's called GeometryReader,
it reads the geometry and you adapt to it
so you can change your
font and things like that.
The safe area thing that we mentioned,
that safeAreaInsets that the
GeometryReader tells you about.
The safe area is best visualized
by thinking of the notch on the iPhone 10.
Most of the time, you don't
wanna be drawing up on the size
where the notch is up
there, not always though.
Sometimes you might actually
wanna draw up there.
And there's other safe area things too.
Sometimes Views will add adornments
or the way they kind of draw on screen
where they don't want you
drawing in certain spaces
so they kind of create this
safe area for you to draw in.
But if you wanna go outside
your safe area, you can,
and the way you do that is
by the View modifier here,
edgesIgnoringSafeArea, and
you specify which edges
that you want to ignore that safe area.
So if I say edgesIgnoringSafeArea top,
then that ZStack and everything in it
is going to ignore that
there's a safe area on the top
and just draw right up
underneath that notch up there.
And so it could be photo
viewing app or something,
maybe you wanna go all
the way to the edges
so you can do that here.
Let's talk a little bit about
how containers do what they do
which is offer space to their Views
and then position them.
It offers space with this modifier frame.
Now, I'm not gonna talk
about frame in detail here.
You can go look at the documentation frame
has quite a lot of arguments,
ideal width, minimum width,
all of these kinds of things
to try and communicate
to the Views here is where the
space that I'm offering you.
So that's all for space.
And then once the View
has chosen its own size,
then we're gonna use this
modifier position to put it
somewhere in our coordinate
space before the container.
So that's how they do that.
Pretty straightforward.
Stacks, for example, would use
their alignment information
and the spacing and all that
to figure out where their Views should go,
and it would set this CGPoint
which is the center of the Views,
at the CGPoint for each of the Views.
By the way, it's kind of a
cool little modifier here
called offset which will offset the View
from wherever container put it.
So you can let the container do its job,
put the thing somewhere and
then you can still offset it
a little bit by something.
And the container could do this
but also someone else could do it.
The View could do it to itself.
I wanna be offset by a little bit.
So offset is kind of a fun little one.
We don't use offset that much
but I just wanna let you know it's there.
Now, for Memorize, we're gonna use frame
and position to create
our own Container View
which is kind of like a stack.
It's gonna be called a Grid
and it's 2D, rows and columns,
instead of just a horizontal row
which is a pretty sad-looking game,
if I do say so myself right now.
And we obviously want that
to be rows and columns.
We'll do that by using frame and position
to create our own container.
So we're gonna go back to the demo today.
I'd love to have time to
actually go do that container,
but we don't have that,
end of lecture three here.
I do have a short demo
though I'm going to do
just to show you how GeometryReader works.
And what we're gonna do is
what I've been talking about,
make our font, our emoji font,
size better to the space we're offered
using a GeometryReader.
While I'm there, I may
do a quick little thing
where I'm gonna show you the best way
or the kind of way we've all agreed on
to collect magic numbers in our code.
If you already have one in
there like cornerRadius 10,
that 10 is a magic number,
it really shouldn't be
embedded in our code.
There's kind of a canonical
way in Swift to take that out
and put it in its own little space
so it's well-documented and typed.
Now we'll start our next lecture though,
using GeometryReader and also generics
with protocols and functions as types
to make this beautiful
little simple Grid View
that's just gonna be like an HStack.
We're gonna replace our HStack
by just using this Grid View
and make our cards be in a nice grid.
So let's hop into that demo
and then that'll be the
end of this lecture.
Now in your homework,
you were asked to adjust
the font choice here
to fit really small cards
because small cards,
the font we chose,
large title was too big.
And that might have fixed
it just for small cards
but I made you do that almost to realize,
well, that's no good solution,
especially when we're
in landscape where even
large title is way too small.
So what we really want is for our card
to pick a font that uses all the space.
That's really what we wanna do.
So how are we going to do that?
Well, we're gonna do
that with a special View.
It's another View, just
like HStack is a View
and ForEach is a View and ZStack
is a View, Text is a View.
These are all just Views.
There's a special View
that is called the GeometryReader View.
So GeometryReader has one argument
which is the content that
it's going to display
inside of itself which is just
another View like our card,
the ZStack that will make our card,
but it has a nice argument
here called geometry,
just like ForEach was a View
that had an argument here
but it also had this content argument
and it provided the Card that
it was iterating through.
Same thing here,
GeometryReader, it has content,
asks for a View, but it
provides this special geometry.
And we're gonna look
at this little variable
that is given to you inside here
'cause we can look at this
and see what the size of our View is.
Now notice that when we put our code,
our ZStack, inside of a
GeometryReader content,
we got these requires self.thing.
So to go through here and
do a fix out of this one,
fix that one as well, pick that up.
So what is this?
Let's take a look at the
documentation for GeometryReader.
So I'm gonna do Option + Click
to look at GeometryReader.
And you can see here
struct GeometryReader.
It's got a don't care called Content
where that Content is a View.
And so now, hopefully, you
are starting to understand
what that means, where
Content is a View, right?
Content is a don't care,
View is a protocol.
So we've turned that don't care into a,
well we care a little bit, right?
We care that the Content of
a GeometryReader is a View,
but otherwise it can be anything it wants.
And so I'm gonna open
that in the documentation
and take a look, see what it says.
And here's the init.
You can see that it takes
this one argument content
which actually you should be
recognizing this syntax here
because really, it takes a function
that returns this Content don't care,
which we know is actually a
we care a little bit Content
where Content colon View
so we know it has to be a View.
And so that's a function,
function that takes an argument,
GeometryProxy, and returns
this don't care Content.
Don't worry about that @ escaping there.
We'll be talking about
that in the weeks to come.
So let's click on this GeometryProxy
and see its decoration.
Here it is, and you can
see it has the things
we talked about in the slides.
Now I'm gonna focus obviously
on the size right here.
So let's click on that.
And you can see that
size is just a CGSize,
it's get only, it's read only.
It's just going to tell
us the width and height
that we've been offered
to draw this View in.
So that's exactly what we want.
We want this geometry size right here.
And what I'm gonna do is
I'm gonna have my CardView
take ownership for setting
its own font right here.
So it's gonna go to font.
So I'm no longer setting
the font in the game itself.
I'm letting the Card set its own font
which is probably better
encapsulation anyway.
Why we're here, of course,
we don't need this colon
content thing just like HStack
and all these other things.
We can get rid of that and get rid of that
so this looks a lot cleaner there.
So we wanna do a font
here, so write some font,
whose size is based on that geometry size.
So I'm gonna create a system font,
just different ways to make
the system font right here,
styles and sizes.
So I'm just gonna pick size.
I want a CGFloat with
the size, the point size.
And remember the point size of font
is related to the point width
and the point height there.
So I'm actually gonna pick
the minimum of the width
to the height 'cause I
don't necessarily want
to depend on knowing what
my aspect ratio here is.
And so I'm just gonna pick
the minimum of those two
to make sure that I pick a font that fits.
So that's min geometry size width
and the geometry size height.
So I'm using that size var that we saw
that's in this GeometryProxy right here.
And maybe we could just try this.
Let's just pick a font size
that is the minimum of these two
and just see what this looks like.
So let's run this.
Look at that.
So that is a much closer size
and you even got smaller.
But this is actually a little too big.
It's got a little too big there.
Maybe that's just because plot point sizes
aren't exactly the width of the font.
It may probably closer
related to the height,
but also we do put a little
stroke around the edge
that takes away a little space as well.
So probably I need some sort
of constant multiplier here,
like maybe times 75%,
0.75, something like that.
And 75% actually looks pretty good.
Looks good when it's big,
looks good when it's small.
Before we finish up lecture three,
I wanna take just a moment here
to talk about a coding style issue.
I don't often talk too
much about coding style,
purely for time constraints
in these lectures
but this time I am going to mention this.
'cause it's a little bit of a segue
into how we're going to
structure our View code
to make it as readable as possible.
This problem I'm worried
about is these magic numbers.
We've created these blue magic numbers
and started to sprinkle them
out throughout our code.
That's not very good coding style.
SwiftUI is declarative.
We're essentially declaring
the UI directly here.
We're not calling functions
to tell it to build itself,
we are declaring it right here.
And when we do that,
these blue numbers end up
being kind of the knobs
that we can turn to fine
tune the way our View looks
and get it just right.
Well, right now our knobs are just spread
all over the place here.
Really nice if we could
have a control panel.
So I'm going to create
a little control panel.
I'm gonna comment here to
MARK it drawing constants,
I'm gonna call it,
and put all my drawing constants down here
as just vars and lets and
functions on my struct.
And this idea of putting
vars and lets and functions
in your struct to clean up
or fix magic numbers in
your View, really important.
You're gonna see that most Views have
a few vars and lets and funcs down here
to make this look as clean and
understandable as possible.
So let's use this idea to get
these magic numbers out here.
These happened to be constants.
So I'm gonna use let.
Remember, let is like var except
for let means it's a constant.
So let's do the corner radius
up there, cornerRadius.
You might think you could
say cornerRadius equals 10
but this doesn't work and
I'm gonna show you why here.
If you hold down the option
key, remember, and click,
it'll tell you the declaration
of this thing that you clicked on.
In our case, it's our cornerRadius
and it has been typed as an Int.
Remember that if we say
let a var equal something
and we don't specify it's type,
Swift will infer it.
And here it's looking at
this 10 and referring,
looks like an Int.
That's not what we want.
All of these blue numbers
in here are CGFloats,
floating point numbers we use to draw.
Now, I can't even just say 10.0.
If I do that and Option + Click,
it thinks it's a Double.
These are not double precision
floating point numbers.
This Double struct is not
the same as a CGFloat struct.
So I have to explicitly
type this, CGFloat.
And that's not necessarily
that burdensome or onerous,
it's kind of nice in
a way to remind myself
these are drawing constants down here.
Let's do our other ones.
We got the edgeLineWidth.
There's another constant we have up there,
that number three,
and of course we have this 0.75
which is really like a
scaling factor for our fonts.
I'm gonna call it fontScaleFactor,
that's also a CGFloat, 0.75.
Now that I have these down here,
I can replace all my
magic numbers with these
and this becomes the
knobs on my control panel.
So let's do that.
We've got this one.
I'm gonna copy and paste,
make this go a little quicker.
And over here, this three
is this edge line right here
and our fontScaleFactor
here is this 0.75 here.
This is nice.
This actually makes this
code read nicely as well,
pretty much like the English language,
trying to understand what's going on.
However, you notice it's introduced
a whole slew of errors here.
They're actually all the same error.
It's the dreaded explicit self.
to make capture semantics explicit.
And we could just, maybe
fix this one and click here
and then fix this one and
I click it and fix it,
but I'm gonna show you a trick
for avoiding this self. thing
in this common case of GeometryReader.
So whenever you do a GeometryReader,
the stuff inside is always gonna complain
about this self dot.
By the way, same thing with ForEach.
ForEach stuff inside is gonna
complain about self dot.
Not every View does that but those two do
and they're commonly used.
I'm gonna create a func which
I'm gonna call body for size,
CGSize, and it's gonna return my body,
so that's gonna be some
View just like I can have
some View be the type of this var,
I can have some View be the
return type of this func.
And I'm gonna put my body,
to cut it out of here, and put it in here,
and then just call this
function in my GeometryReader,
self.body for the geometry size.
This code cleaned up
actually kind of nicely.
This makes perfect sense.
And this code now is no longer embedded
inside this GeometryReader like this
so you don't need these self dots.
So I can get rid of that one
and that one and that one and this one.
All those self dots are gone.
What's more here, I didn't
pass the entire geometry
to this body, just the
size, the geometry size var.
So down here, I don't
need to say geometry.size.
This geometry.size is now just size,
this size that I passed in.
So that made this code look a lot nicer
and I almost always recommend,
at least for the next couple of months
until they put that Swift
self.change in there
so the self. is not causing
this problem anymore.
This is a real clean way to have this code
not have to worry about self dot.
And you can do the same thing
body for item in a ForEach
'cause ForEach is gonna cause
that self. problem as well.
We could do one other thing here.
Here, I could have a func
for the font size called
fontSize for size,
have it return a font size
which is also a CGFloat,
and put this code right here down in here
so that this just reads fontSize for size.
You might think this is
a very simple expression,
I don't really need to turn
it into its own func here,
but again, you can't make this too simple.
You really want to have
this be simple as possible.
And sometimes you're
forced to make it simple.
These ifs, as I explained,
can't be arbitrarily complex expressions.
So sometimes this needs to be a function
that returns a Bool,
not in this case, 'cause
this is simple Bool.
But making these little
one liner functions
that make this body look
cleaner, very common.
Here, we might even not have
to have this fontScaleFactor
be its own separate let
because you can kind of
think of the font size
as just part of the drawing constants.
This is the constant font
size for a given size.
So that's it for lecture three.
And we're gonna dive right back in,
start with lecture four
and continue with this demo
and make our HStack up here be a 2D grid.
We're gonna learn a lot
of stuff doing that.
- [Announcer] For more, please
visit us at stanford.edu.
