[MUSIC]
Stanford University. >> Okay,
well welcome to Lecture 6, Stanford CS193P,
Fall of 2017. So today I'm gonna continue that demo that
I started last time. It's gonna be gigantic demo today,
covering mostly stuff having to do with custom views.
Then I come back to the slides,
just a few brief slides on multi touch and
how we do that. Then we'll go back to the demo and add some
multi touch gestures to our little playing card thing.
Here's the old slide of what's you're gonna learn today,
which you go back and look at this slide after the demo.
Then try to decide, did I learn that, well, we will find
out. By the way between last lecture and this lecture, I
went ahead and finished off a custom string convertible for
all three of these things. I just made suits custom
string,convertible return its raw value, remember its raw
values are these little equals things here. And then rank,
I had to actually implement a little description,
right there, where I returned A for the one. And then
a string version of a number, or the kind J, Q, or K. But
once I implemented custom strings convertible on
all three of these things. And then this code we had back
here where we just printed out ten random cards,
that prints out a lot nicer on the console.
So let's take a look and see what it does now.
See, it just prints it out here as kind of an abbreviated
version, which is, if you're debugging, it's a lot nicer to
be printing your cards out and seeing that.
And you might want to do the same thing in your assignment
number three as well. So that's it for that.
We've completely finished our model for
this MVC, that we're building here, this app,
this PlayingCard, so we have a deck of playing cards.
So now it's time to dive in to drawing these, these cards.
And we're gonna do that with a custom UIView subclass, which
is I'm gonna call PlayingCard view. Now you create a custom
view in the same way you create other classes.
So you're gonna do File > New > File. But here instead of
picking Swift File, which is like a UI independent thing,
you're gonna pick Cocoa Touch Class. That's because our UI
view is a subclass of a cocoa touch or UI kit class.
So I'm gonna call it, playing, playing card view,
it's gonna be subclass of UIView. A lot of other UI kit
things can be subclassed here, but the one I want is UIView.
And it says, where you wanna put it? By the way,
I just wanna remind you all, some of you are putting your
files at the top level, the project level, so they're
ending up like next to your X code project right there.
You really wanna be putting them down a level in here.
This is where we collect all of our classes. So just
a little reminder there, we're seeing that on the homework.
And so here's my UIView subclass, look at this, see?
Subclass of UIView, that's great.
And it even gave me a stub of a very important method here,
which of course, is our draw rect. Now, you notice this is
commented out, in this stub, that's because this iOS
actually looks to see if you have a draw rect.
And if you do, it makes an off screen buffer for
you, and all kinds of preparations for you to draw,
okay, and that's not cheap, it's not free.
So if you don't actually draw in your draw rect,
then you would want to leave it commented out.
Now why would you ever have a UIView, or UIView subclass
that doesn't have a draw rect? Well, that's actually quite
common, you do all your drawing with subviews,
consider UI stack view, right? It's a UIView, it does all its
drawing with views that are stacked inside of it.
It doesn't do any actual drawing itself, it has no draw
rect, right? But we are gonna have a draw rect, of course,
because we are going to be drawing a playing card.
Now I'm actually just, for example purposes here,
I'm gonna draw some of my card with sub views and
some of my card with this draw rect, okay. So that way you'll
get to see one view that actually does both. And
in your homework assignment, you're probably gonna have at
least one view that does subviews, and
at least one view that has a draw rect. So
you'll be able to see all that at action, in action here.
All right, so we got this PlayingCardView. Let's go back
over to our storyboard right here, and put a UI view,
a PlayingCardView basically, into our UI, okay.
So how do we do that? Well, how do we put views in our UI?
We go over here to utilities and down at the bottom,
maybe we drag out a button, or we drag a label.
And of course where is playing card view? Well, it's not in
here, of course, cuz these are all just the things that come
with X-code. But I can drag out towards the bottom here,
this guy, View, which is a generic UI view.
So I drag him out here and his class or
his type is just UI view. I'm going to make my background
a different color so we can see him a little better there.
So I'm just gonna select my background and change it to,
oop, orange, I love orange, there's orange.
San Francisco Giants colors right there.
So here's my kind of generic UI view.
And I don't want this to be a generic UI view,
I want it to be a playing card view, okay, cuz that's what
I've been working on. And the way we do that is
with the different inspector on the right.
You see we've been using this inspector right here,
the attributes inspector. Right next door to it is this
guy. This is the identity inspector,
it inspects the identity of the selected thing.
So here I have a view selected and it's of type UI view,
you see the class? But I can go here and change it to be
a playing card view. So now this is a playing card view,
and any time the system needs to draw it, it's gonna use our
draw(rect) right here. It's a code that we've written.
So that's awesome. Now I'm gonna do a little bit of auto
layout here that you've seen before. So this is nothing
new, but I'm just gonna put this up in the edge here,
put this one down here and I'm gonna pin it to the edges.
So my PlayingCard is gonna be kind of tall and
thin in portrait mode and kind of short and wide in landscape
mode, but that's okay, we'll fix that later. So
I'm just gonna drag up to the corner and set my leading and
top spaces to be pinned. And I'm gonna drag Ctrl+drag down
to this corner and set my trailing and bottom.
So they'd start there, so now if I go and
go into landscape mode right here, you can see that it
pins to the edges, so I have this funny shape. Now,
I'm doing this mostly at the start here because I want to
show what happens inside your view when your bounds change.
Because here when we rotate, our bounds are gonna be
changing very dramatically, from tall and thin to wide and
short. So before we dive into doing a playing card,
I'm just gonna do a little bit of drawing,
show you how drawing works with core graphics and
UI bezier path like we talked about in lecture. So
lets first draw a circle, just a circle in the middle of our
view using core graphics, and see what that code looks like.
So in core graphics, we always get the context first.
So we can't draw in core graphics without a context and
we get that in our drawrect by doing this
UIGraphicsGetCurrentContext. Now, this could return nil,
that's why we do if-let, but it will never return nil
inside your drawrect. Okay, it might turn,
return nil in other contexts, but in this environment,
it's always gonna return,
but we're still gonna do if-let right there. We could
do exclamation point where we're just gonna do if-let.
So now that I have a context, now I can tell the context to
do certain things, move to, okay, I can do move to.
I can do add line to, things like that. Add curve,
I can add these things that basically are drawing a path,
right, like a line moving around. So I'm gonna
make a circle. So I'm gonna use one called addArc.
An addArc is kinda cool, it just like takes a point and
then circumscribes a big arc around a circle.
And I'm just gonna use that to go all the way around and
create a circle. So when addArc is creating a path,
it wants to know what's the center of this circular
path that you're going on.
And I'm gonna make it be the center of my drawing area. And
what rectangle specifies my drawing area? Bounds, okay, my
var bounds does that. So I'm gonna create a CGPoint here,
which, whose x coordinate is my bounds midpoint.
And I'm gonna create the y coordinate is my bounds
midpoint in y. So I'm specifying right in the center
of my drawing area, which is my bounds. The radius,
I'm just gonna do 100 points, nice big circle.
The start angle and end angle here are in radians,
not degrees, not 0 to 360, it's radians, 0 to 2 pi.
Does everyone know radian? If you know what radians are.
Okay, everybody, great, so 0 to 2 pi. And 0, by the way,
is off to the right. 0 is not straight up,
as you might imagine, it is off to the right.
So I'm gonna start off to the right and
I'm gonna go all the way around my circle.
I can go either clockwise or counterclockwise,
it doesn't matter cuz I'm going all the way around. So
how do I go around? Well, that's 2 times pi. And
there's a really nice, little constant here,
CGFloat.pi. Okay, and that's how I can get pi in a CG,
as a CGFloat. And I can go clockwise or
counterclockwise, it doesn't matter. All right, so
now I've created some path, some drawing here.
So I can do other things in my context like,
I can set the LineWidth, for example, not the LineCap, but
the LineWidth, 5 points wide. That's a reasonably thick,
not super thick, but reasonably. I, of course,
can set the colors I wanna draw with using these static
vars in UIColor. Like let's say, green for our setFill.
That's our favorite fill color. And UIColor.red for
our stroke color. Okay, so I can set whatever colors. And
then I can ask the context for example, to stroke the path.
So let's do strokePath here. And you'd think I could then
say context.fillPath. Let's see if this will stroke and
fill, and it won't. And the reason for that is that when
we draw in a context, it's actually slightly different
than using that UIBezierPath I showed you in the slides.
In the context, when we do a strokePath like this,
it consumes the path. Okay, it uses up the path. And so
when we do the fillPath on the next line, there's no path.
We'd have to start again. So
that's one of the big advantages of UIBezierPath. So
let's do this exact same thing here, but using UIBezierPath,
all right? So I'm gonna say let path = UIBezierPath.
We'll start with an empty one.
It had, I'll show you later how to create a BezierPath to
start with a path. And then I can do the exact same things,
almost exact same methods as above. In fact,
I'm gonna copy and paste this exact same code right here.
The names are slightly different, in UIBezierPath,
but they're doing exactly the same thing, like lineWidth.
You don't say setLineWidth,
it's just a var on that objects. So you set it to 5.0.
You still set your colors by doing this. And
here the difference, though, as I can say path.stroke.
And that path, that UIBezierPath,
still exists as an object, so I can say path.fill.
I could also move the path over or
shrink it down a little bit and stroke it again.
You see what I'm saying? So I can use this path that I built
this arc over and over and over. That's the whole point
of kind of building it in this struct here, or this class,
UIBezierPath. So we'll get rid of that.
And let's see what this does right here. And it's gonna be
very similar, but, of course, it's going to stroke and
fill that path. Oops, did I press play?
Okay, there it is, you see, stroked and filled there.
All right, now while we're here looking at a circle,
I'm gonna do something interesting.
I'm going to rotate this phone to landscape. And what shape
do you think we're gonna have here? Anyone wanna guess?
Unfortunately, not a circle. We want it to be a circle, but
it's an oval. So why did we get this? Because by default,
when you change the bounds of your view,
it just takes the bits and scales them to the new size.
Which sometimes that might be what you want, but
a lot of times, this is definitely not what you want,
right? So how do we stop this? Well, what we want it to do is
to call this code again when we change our bounds and
have us draw the circle again in the new space. So
how do we do that? Let's go back to our storyboard here,
take a look at our view. If we inspect our view, at the very
top of the inspector, the very first thing is Content Mode,
Scale To Fill, right?
So it scales the bits to fill when the bounds change. And
we want to change that to be Redraw. So Content Mode Redraw
means call my draw rect again when my bounds change. So
now when we run, we get to see our circle. And
when we rotate to landscape, i's going to redraw, and
thus, draw it as a circle, which is what I intend.
Tha's what our drawing code that does,
it draws a circle. So that's important to note,
especially in your homework. You're doing these set cards.
You got your squiggles, and your diamonds, and all that.
You, when, if your bounds were to change in a set card,
you wouldn't want it to like squish it into some other
shape. All right, so that is enough of kind of
taking a look at drawing with Core Graphics and with
UIBezierPath. Let's settle down now to drawing a card.
Now what are the parts of a card? We've got the corners,
right? The corners of the card, which is the rank and
the suit in the corners. In the middle, we've got either
a face card image of some sort or we've got a bunch of pips.
Those little things are called pips. The hearts and clubs and
diamonds, we got a bunch of pips in there. So that, that's
how we got to build our card. But actually, the card has
another thing, which is almost always has rounded edges,
right? You know, if you've ever played cards,
you don't want sharp edges cuz it catches on things and
stuff like that. So you want nice rounded edges. So
let's start by back, drawing the background of our card
as a rounded rect. Now you actually know how to do this
using the layer of a UI view,
which was in assignment two hints. But
I'm gonna draw it directly, using a UIBezierPath. So I'm
just gonna say here, let path, actually, or you can call it
a roundedRect cuz that's what I want, in my background,
= UIBezierPath. And I'm gonna use a different constructor
than I used before. And you see there's a lot of them,
ovals and rects, but here's one for roundedRect. So
I'm gonna get, do this roundedRect.
It's asking me where you want your roundedRect to fit into.
So I obviously want it in my bounds.
It's gonna fill my entire bounds.
And then this corner radius is how many points the radius of
the turn of the corners is. And for now I'm gonna set
that to a magic number. We don't really want blue,
which is these literals. We don't want these in our code.
These are bad and I'm gonna get rid of that pretty soon
here. Why do we not want those? Because if we actually,
literally have magic numbers like that, we wanna collect
them all into some area where we have our constants.
So we can modify them and
understand what we've chosen and all that.
We don't spread it all out through our codes.
If we're ever going to change the constants,
we're looking at a round, round form. But for now, we'll
leave it this way. All right, so I got my roundedRect.
The first thing I'm gonna do to my roundedRect actually is
I'm gonna tell it that I want it to be the clipping area for
all my drawing. So as I've had this nice roundedRect,
which is the edges of my card, I don't wanna draw outside
that roundedRect. By Rect, my drawing all has to be inside.
Now, I don't think I'm gonna write any code that goes
outside. But in your assignment three, you might.
Because in assignment three, you're gonna have to draw
the squiggle shape. With arcs and lines or something, and
then one of the fill modes is striping. So you're gonna have
to draw up stripes in there. Well, imagine trying to draw a
stripe that goes from one edge of a squiggle to another edge.
This would be almost impossible.
Much nicer if you just have your squigle be a path,
add it as the clip, now you can draw those lines sloppily,
like you're a two-year-old in a coloring book,
draw them paths. And it'll get clipped, so
it's all inside the squiggle. You see why you want clipping?
So here I don't care so much, but I just wanna show you what
it looks like to call that. Now, I want the my card to be
white of course, so I'm gonna say UIColor.white.setFill().
And then I'm going to fill my roundedRect. My roundedRect is
just a Bézier path, so I can say fill.
So let's and see. This worked, cuz now, hopefully,
we should have roundedRect for our card. And we don't.
See it still has sharp edges up here, see these sharp edges
right here? Why does that still have sharp edges?
Well, actually, this code worked perfectly. It drew
a perfect white rounded rect on a white background. So
we cannot see it, it's sitting there on a white background.
So we need to go back to our storyboard here,
and change this so that it's not white background. So
what color background do we want for this thing? Actually,
we want it to be clear. Because when we draw a rounded
rect, we wanna see through the parts of
the corners that is rounded, to whatever is in
the background. So we want it to be clear. But
as soon as we start talking about clear and
see-through in our view, we need to talk about
this switch right here, the is opaque switch.
And as I said in the lecture, this is by default on, and
it's assumed you don't have any see-through parts,
no transparency, and it can be more efficient when it draws.
So if we do use transparency, which is less efficient,
but we need it here,
because we need our corners to show through,
we have to turn this off. So don't forget to turn that off,
if you're gonna do anything transparent in your view.
All right, now we have rounded rect. You see the rounded
corners right there, and we have it in both landscape and
portrait, okay. So that's good. All right, we're off to
a good start. Now, we're gonna do our corners.
So our corners, remember, are rank and suit, and I'm going,
it actually will probably be easier to draw the corners
with an NSAttributedString, directly in my drawRect.
Probably could do it in five lines of code.
But instead, I'm gonna use 15 lines of code, and
do it with a UI label.
Because I wanna show you how you can build your UI view,
out of other views, by making them subviews of yours. Then
we'll do some other drawing with drawRect, which will also
be only a couple lines, all very efficient to do.
So how I'm gonna do this on my UI label, is I'm gonna create
a UI label that uses an attributed string as its text.
And this attributed string is going to look like this.
So if it's gonna have, for
let's say, let's pick five of hearts. So
I'm doing the five of hearts, and this is the corner of my
big card. So I'm just gonna create an attributed string,
which is five carriage return,
heart. That's the attributed string I'm gonna create. To
make this work, my attributed string needs two attributes.
Attributed strings have attributes, I only need two.
One is the size of the font. I wanna make the font big if my
card is big, small font if my card is small.
The other thing is this needs to be centered,
cuz I don't want this five over here, lined up with
the left edge of the heart. I want the five centered over
the heart, right? And I might have like a ten of hearts.
This ten might actually be wider than the heart. But
I want these two things centered. So
I'm gonna show you an attributed string,
how to do fonts, and how to do centering of your text. So
let's create a little kind of utility function.
Pretty generic function. I'm gonna call it,
it's gonna be private, I'm gonna call it
centeredattributedString. So what this function
is gonna do is it's gonna take a string and a font size, and
return an NSattributedstring that's centered with that font
size. So it's gonna take a string,
some string as the string that we're gonna do. In our case,
it's gonna be five carriage return heart, and it's gonna
take some font size. Font sizes are CGFloats of course,
all photo point numbers in drawing are CGFloat, and
it's gonna return an NSAattributedString. So
that's what this little function is gonna do.
Because we need that to draw this corner piece. Okay,
let's do the fonts first.
So I'm gonna create a font. And to do that,
I'm gonna use those preferred fonts. Because this card,
what's on the card, is kind of user information, so
I wanna use a preferred font, not like the system font or
anything. So I do that with UIFont,
static method, preferredFont(forTextStyle.
In the text style, I'm gonna use is .body, the body font,
because it's really not a caption or a footnote or
a headline, it's kind of body text. But I'm gonna scale it,
and luckily, you can just say withSize to a font, and
give it the fontSize you want,
which is this argument to my method. So this is great, so
I've created a preferred font, the body font, and
I've scaled it to the right size that I want.
I'm gonna have to figure out what that size is for my card.
But there's one big problem with this.
If someone goes on, let's go to the simulator here.
Where's my simulator? And
if I go over to Settings on my device, and I go to General,
Accessibility, Larger Text. Look, I have a little slider
that can change the size of the text in all my apps.
Well, all my apps won't include this app unless I deal
with the fact that I fixed the font size here. So what I
really want, is something that's this font size, but
if they put that slider up, I want it to be bigger and
if they put that slider down, I want it to be smaller.
Luckily, there's a great way to do that, which is you can
just reset the font to be equal UIFontMetrics.
So this UIFontMetrics has a great feature in it, where you
can create font metrics for a certain text style.
Again, the body font right there. And
then you can get a scaled font from another font. So
you just give it a font, this one up here that I created,
and it will scale it based on that little slider. So
don't forget this line of code. Otherwise, users
who are visually impaired, or even just old guys like me,
who, you know, need big fonts, we set that a little higher,
and your app is not gonna do it. Your cards,
your playing cards, are gonna still have small text, so
don't forget this line, if you're doing fonts.
All right, how about the centering,
I wanna center the five on top of the heart.
Well, we're gonna do that with another little type, which is
called paragraphStyle. And I'm gonna create
an NSMutableParagraphStyle. So paragraphStyle
encapsulates all the things about a paragraph,
like its alignment and things like that. And so I just set
whatever I want in there. Like in this case, I want
the alignment to be set and I'm gonna set it to center. So
that makes the whole paragraph of text there be centered
horizontally. So that's it. Now, I can just return
an NSAttributedString with those attributes,
and I'm good to go. So let's use the same
exact initializer we used before. So here's the string.
That's the argument to the function right here, string.
And then the attributes right here,
I'm just gonna put the dictionary right in.
I'm not gonna put it in another bar or
anything like that. Let's just put it in. And so I
do NSAttributedStringKey .paragraphStyle for
example. So that's one of the keys, and the value is this
paragraphStyle I just created, and then I can also do .font
of fonts. Notice, I don't have to type this every time.
In fact, I don't even have to type it the first time,
because Swift knows what type of argument this thing takes.
So, it automatically will infer that part of this. So
that's it. Okay, nice reusable function that will create this
kind of attributed strength. So
now I'm gonna create a little private var, which I'm gonna
call cornerString. String,
and it's just gonna return a centeredAttributedString with
this, the five over the heart. So somehow I need to have my
rank plus a carriage return, +suit, and then I'm gonna,
woah, then I'm going to need, some font size.
Who knows what that's gonna be?
Well I have to talk about that,
because its got that font size. It's gonna depend
on how big my card is. My card is big, that's gonna be big.
So we have a couple of things to deal with here. One,
we need the rank and suit. So the playing card has to have
some way to set the rank and suit. Now, I'm gonna make my
rank be an int, and I'm gonna make my suit be a string.
Now this is different than the model we had.
The model had rank and string be enums, remember that?
But who cares? This is a view, it knows nothing about that
model. This is a generic card drawing view.
It does knows nothing of that particular model. So
the fact that it represents its rank and
suit in a completely different way, perfectly fine.
Whose job is it to translate between model and view?
Of course, the controller. So yo're, w're gonna see code
in our controller that translates between the models,
thought of what a rank and suit is in this view.
I also, I don't wanna have to
be an initializer there, it's used as no initializer. So
let's start, let's start with this 5, 5 of Hearts.
Let me go grab a heart from, over here. Here's heart,
copy. All right. So we got 5 of hearts right there. And
there's one other thing too, which is, is this card face
up or face down? So I need a, isfaceup. She's a bull, and
we'll start with a face up, let's say. Now,
when you have vars like this in a view that affect the way
the view would draw, you have to think about the fact that
if this changes the rank, Your view needs to redraw itself,
right? If you change the rank, you gotta redraw. So
how do you do that? This is a really great use for didSet.
So when this rank changes,
someone sets the rank to 11, for a Jack, we gotta redraw.
And how do we make ourselves redraw? Everyone, remember?
setNeedsDisplay.
So that's gonna cause our drawRect to be called,
eventually. So we can't call our drawRect directly. We just
have to tell the system, hey, we need to be displayed.
Our view has another little thing that needs to happen.
We have subviews to drop part of our view, so
we need to have those subviews laid out.
Now, we're not using Auto Layout in our subviews,
we're putting them where they belong in the corners, but
we still need to say setNeedsLayout as well. So
that our subviews get laid out.
Now you don't have to say this if you don't have any subviews
that need laying out, or
that aren't affected by the rank changing. In our case,
it definitely does change the rank. So
we're gonna do that for all of our little public vars here,
because if people change any of these things,
it's gonna change the way our card looks.
Don't forget this piece right here, always gonna want that,
either one of these two, or both, on every time you have
a public var, that someone can change the look of your card.
Okay, so now, we have rank and suit.
Unfortunately, rank is in int, so I can't say rank +suit. And
then also, I have this problem with this magic number here,
somehow I have to pick a font size.
So in order to speed this demo up a little bit,
I actually have a little extension to my playing cards.
Oops, there it is, this little extension right here.
This is the entirety of it, it's not very big. And
this has captured all of my little blue numbers,
my magic numbers, into a struct as static lets.
So this is how we do constants in Swift.
We make a private struct, we give it a name, sometimes it
might be called constants. I've called it SizeRatio,
because all of my constants are about the ratio
of the corner, or of a font, to the size of my card.
So I call this SizeRatio.
And then in here, I have the cornerFontSizeToBoundsHeight,
I have the cornerRadiusToBoundsHeight,
I have the cornerOffsetToCornerRadius,
I have the faceCardImageSizeToBoundsSize.
These are all ratios that I've picked, that I think will
look good. Then I even created some little computed
properties like cornerRadius, which takes the height, and
multiplies it by the ratio. So here's what it looks like to
use a constant that's declared like this, SizeRatio.whatever,
or if you have a constants, it might be constants.whatever.
You see how this kinda looks nice right there.
That's how we do it.
So I have these 3 things,
cornerRadius, cornerOffset, and cornerFontSize which would
have allowed me to get rid of blue numbers.
Instead, use something that's with respect to the size of my
cards' height. I also threw this whole guy in here,
rankString is just a var that turns 1 into A and 11 into
J and 12 into Q, and all the other ones into a number. So
that I can have a string that allows me to go up here when
I'm creating this little string right here. Instead of
saying rank plus character term plus suit, I'm gonna say,
rank string plus character term plus suit.
This, this is the, this means character return, right?
Go to the next line. And so now,
my font size can be this cornerFontSize, one of these,
once I created down here. And similarly, my cornerRadius
right here which was 16 can now be cornerRadius.
That's another one of these that I created. So see
how I've segregated off all of my constants into this nice,
little I even used some extension.
It wouldn't have to be an extension, but
I just put it off in its own space. And while I was at it,
by the way, I also added some extensions to CGRect and
CGPoint like zooming a rect, or sizing into something, Or
getting the left half of a rect, just for convenience.
It's gonna make my code look a little cleaner.
And you already know about how to do that.
We did that with the art for a random and int,
stuff like that. Okay, so we're getting very close to
making this work right now. All we really need to do is
create these UILabels. So I'm gonna create a var for them,
private var. I'm gonna have an upperLeft,
upperLeftCornerLabel, okay,
which is gonna be typed UILabel. And then,
I'm gonna have a lowerRightCornerLabel
to UILabel. Now, I need to create this UILabel,
so I'm gonna create a little function to do that,
private func createCornerLabel, and it's
just gonna return a UILabel. This is gonna be really easy.
I'm just gonna create a UILabel and return it, but
I have to do a little bit of configuration of this.
We'll get to that in a second. So here, instead of this
declaring this label, I'm gonna say =createCornerLabel.
And then here, createCornerLabel, oops,
not Repl_host, how about createCornerLabel.
All right, now, this is going to Once it catches up to me
and compiles, gonna create this error.
What is this error right here? Cannot use instance member
'createCornerLabel' Label within a property initializer.
Well, of course, I'm initializing a property here,
and here, I'm trying to call a method on myself. And
we know that until we're fully initialized,
we cannot call methods on ourself. So with this,
this is the old catch 22. So anyone wanna say how we could
fix this? Okay. Lazy. Good job, everybody. All right.
Lazy, exactly. So lazy makes it, so
these things won't be initialized until they
which will be after the thing is fully initialized. So,
are asked for,
this is equals. All right, so we have this UILabel.
What do we have to do to initialize our label? Really
only a couple things. One is I need to set this bar on label,
which is number of lines, because the default is one.
By default, a UILabel has one line. So if I have a two line
thing, like five\n hearts, it would only see the five.
The heart would not be shown. So I'm gonna change this to 0.
I could change this to 2, but I'm going to change it to 0.
What 0 means is use as many lines as you need, Mr.
Label. So I'm taking it to 0. So that's really the only
thing I have to say. The only other thing I have to do with
this label is add it as a Subview of myself.
If I dont add it as a Subview, then it won't be there,
it will never draw. Okay? So I have to add it as a Subview.
So that's all you need to do to create a CornerLabel.
But, I need to position these labels.
I have to put them in the right place, right? So
I should put one in the upper left and one down in the lower
right. So, where do I do that in my code? Well,
I have to do that every single time my bounds changes,
especially for the one in the lower right. Okay.
The one in the upper left is actually near my origin.
It's probably gonna be right no matter what my bounds are.
But the one in the lower left, in landscape,
it's way over to the right and not down very far, and then in
portrait, it's way down and only a little bit across,
right? So that one in the lower right is moving all over
the place when our bounds change like that,
when we rotate or any reason for
reason our bounds would change. So where can we put
some code that does something when our bounds change?
That's what this method, layoutSubviews is for.
To UIView method, make sure you call super,
because UIView is awesome at laying out Subviews.
It uses auto layout. All that auto layout stuff we're doing,
that's all stuff that UIview knows how to layout
your Subviews. Now, these two Subviews,
I'm not doing any control dragging. In fact,
I'm creating them in code, right? I created the UILabel
in code right here. So, I have to do the layout myself, and
layoutSubviews is where you do it.
Anytime your Subviews need to be laid out for any reasons,
this is going to get called by the system. You don't call it.
If you want it called, you call setNeedsLayout.
And setNeedsLayout, the system will eventually call this.
Just like if you do setNeedsDisplay,
the system will eventually call this. Okay?
Very, very similar. All right, so we now layoutSubviews.
All we gotta do is move this UILabel, this upper left, and
lower right labels, move them to the right spot. So let's do
the upper left, that's a really easy one, actually.
So I'm just gonna set my upperLeftCornerLabel.frame.
Remember, frame, in a UIView, is what positions it,
bounds is where we draw, frame sets it. So
I'm gonna set its origin basically equal to my origin,
but offsetBy, so I added this little offsetBy in CGPoint.
It just moves the point over by some amount,
offsets it. So I'm gonna offset it by this
cornerOffset that I have. So the cornerOffset,
which is one of these things I made from my constants here,
that just gets passed the little curve.
I don't wanna draw this with the curve right here, so
I need to move it in a little bit from the roundedRect.
Okay? So that's it. Now, we're not quite there.
We've positioned it, but we haven't actually set
this string on it. So I'm gonna create another little
function here I'm gonna call configureCornerLabel,
and I'm gonna pass that upperLeftCornerLabel to it.
And inside here, it's a little private func. We will pass
this label. We don't really need an external name, because
the name of the function implies the external name,
it's UILabel. So here, I'm gonna configure it.
And I don't actually have to do very much to configure it.
One thing I for sure need to do to this label is set it
attributedText to be my cornerString. Remember,
cornerString is this thing up here.
This little guy just gets a centeredAttributedString with
the rankStrin\n suit of the right size,
depending on how big our card is.
So we definitely need to do that.
What else might I need to do to my label when I do this?
Well, one thing is I want the label to be the right size.
Okay? I want it to be kind of the perfect size to enclose
this thing. Luckily, label has a method called sizeToFit, and
it will size the label to fit its contents. The only tricky
thing about this though, is if that label already has some
width, and you say sizeToFit, it will make it taller and
keep the width. Well, we don't want that.
We wanted to do the whole thing, so I'm gonna say,
label.frame.size = CGsize.0.
So I'm gonna clear out its size before I do sizeToFit.
That way, it will expand in both directions, across and
down. That's a little old trick about sizeToFit you
gotta know there. And the last thing, really tricky thing,
is what about if we are not face up?
Do we draw these corners not face up? Of course not.
We don't want the back of the card to have that. That would
make it really easy to play a lot of games if the back of
the card had corners on it. We don't want that,
so I'm going to configure the label to be hidden,
not highlighted. Hidden, if we're not face up.
Okay? So if we're face down, then I'm gonna be hidden.
So here's the example of using Hidden.
It keeps it in the Subviews, list, in everything,
keeps it in the right position, just hides it. Okay?
Instead, we're gonna draw the back of our card,
whatever that looks like. Okay. It's a good example
using isHidden right there. Okay. It should work.
Let's take a look and see if we can get that upper,
at least this upper left one to draw. There it is.
Five of hearts. It looks good. Let's see if it works when we
go to landscape. Whoa! Not only it's right position, but
look, it's smaller because the card is shorter, so
we don't wanna use half the card with our big font.
So that's good. What about the other corner? Okay,
well the other corner is a little harder to position
because our origin's in the upper left and
we're trying to put away down to the lower right.
But it's not that bad, so let's just try and
do it. This is our lowerRightCornerLabel. It's
frame.origin. Well, I'm gonna build this incrementally. I'm
gonna start by making a point, which is my bounds.maxX, so
all the way over to the right, and y is my bounds.maxY,
that's all the way down to the bottom. Okay? But
I can't put it there. If I try to put it there, here let's
draw a little picture so you can see. I'm drawing the lower
edge now. Okay, here's my lower edge of my card and
I'm trying to put this thing here. So I can't put it here.
If I put it where this is, this would be the origin,
it would be down here, not even on the card. So I need to
move this point first inside the corner offset, then,
the whole distance of the width and height of this
little thing, so I need to kinda make a double jump here
to get this origin up here, so this will draw there. Okay, so
I'm just gonna do double offset by.
The first offset by I'm gonna do is -cornerOffset and
-cornerOffset that gets me pass the roundedRect.
Then I'm gonna offset again
-lowerRightCornerLabel.frame.- size.width,
and -lowerRightCornerlabel.frame.-
size.height. You see how I had to move the origin
back up there, everybody cool with that. Okay, so
that positions it, this is wrong, cornerOffset, right?
So that position is it, of course we have to configure it
as well. So let's just do the exact same thing here but
we're gonna configure our lower right. Because it needs
to be configured in exact same way. And use the corner string
whatever, so, let's see what it looks like. Lower right,
oops. I didn't finish there lowerRightCornerLabel,
all right Okay, whoa interesting.
Well that's not quite right is it?
Okay, it's in the right spot but that five hearts
should be upside down, right? If you look at a card,
a playing card that would be upside down, okay.
So, how the heck am I gonna turn that thing upside down.
Well, that turns out to be super easy
in iOS because every view has a var on it,
lowerRightCornerLabel has a var and it called transform.
And transform is what's called an affine transform,
how many people know what an affine transform is? Okay,
nobody, basically, almost.
So an affine transform is really simple,
it's just a blob, a thing that represents a scale,
a translation, and a rotation. Okay, just those three things.
So you can take a UI view and rotate it, scale it, and
translate it all you want with just this one little var. Now
of course we are positioning things with the frame and
stuff like that, but this is an additional way to control
it's positioning, scaling, and rotation.
Now this is all going to be bit wise translation. So
it's going to be translating the bits. So if you make it
bigger, it might look kind of jaggy, edged, pixellated.
But we're not going to make it bigger. Instead, we just want
to rotate it. So you might think we can just do this.
Let's take the AffineTransform.identity
transform, so that means unrotated, unscaled,
untranslated, just an identity. And
you think I could just say rotate it.
By the way, transform only has three methods.
Rotate, transform, and scale, that's all it's got. So,
if I created a rotated one, how much would I want to
rotate this if I wanted to turn it upside down? Okay,
in radians? Pi, right? Cuz I want to turn,
turn half way around okay, so it's upside down. So I could
just say CGFloat.pi, but this would not actually work.
This is close but doesn't work so let me show you why that's
not gonna quite work. So if this paper here would do this.
Okay, so here's my corner right here and here's where
this five hearts thing is right now. It's, right side up
like this. Actually here we'll do on a piece of paper. So
here's my five of hearts. And I want it to be
upside down like this, right? Okay, that's what I want.
But, if I rotate it, it rotates around the origin. And
our origin's upper left. So if I rotate it, Pi, whoa,
it's gonna be up there. You see the problem? So
it will be upside down but not in the right place.
So I need to both rotate it and translate it. So
what I'm gonna do is I'm gonna translate it first down to
here to its other corner then I'm gonna rotate it. Woho,
it's gonna work.
Okays So let's do that. Where are we, where is my rotator?
Here's the rotator so I'm going to keep that rotated.
I still want to do .rotated but I want
to do a translate first so I'm going to stay .translated
by and how much do I want to translate by? I want to
translate by the whole width and height of my lower right.
lowerRightCornerLabel.frame.s- ize.width and
the lowerRightCornerLabel.frame.s-
ize.height. So I'm taking the identity, I'm translating it
down to the corner, then I'm rotating it. I could also
have kinda translated it to the center and rotate it and
then move back. That's another way commonly to do
that rotation. But here we go, it's upside down and
it works, even in other bounced sizes. Okay,
excellent, so we've used the subview. We've used layout
subviews to make it always be in the right position,
all is looking well. Let's go check and
make sure that our slider,
remember this slider over here in settings. Remember we can
set it larger, let's go make sure this is working.
I'm going to set this to quite a large size font. And
hopefully when I go back to my app,
it should have a large font but it doesn't.
Why doesn't it have a large font? That is weird.
Well actually, it does, it's just it never redrew.
If I change my bounce, and flip back,
now I get see the large font. So that's a problem.
When that slider moves we need to find out that it moved. And
you can do that in view with a function called
TraitCollectionDidChange. So
traits, we're gonna talk about traits in a couple of weeks.
Traits have a lot to do with are you rotate, are you
landscape, are you portrait, things like that are traits.
But also, your size category in general for your font. So
trait collection gets called whenever those things change.
Here, I'm just going to setNeedsDisplay and
setNeedsLayout, okay. So with my traits,
the thing that control how we draw change,
then I'm gonna redraw. So now if we go back,
right now our fonts are big if we set them big,
so they're gonna start out big. And after I go back and
set them to be small over here in my settings,
go back to normal size, oops, sorry. I'm gonna, got that,
what? There we go, so set it back to normal. Just go here,
go back to our playing card and it rejoint normal.
Okay, because it found out that that slider had moved. So
minor little thing you've got to remember to do this and
we'll talk a lot more about traits down the road. Let's go
back, and do a little bit of layout stuff, take a little
break from drawing our card, and do layout. So right now,
we've got this thing where this card takes up the whole
space, actually, I'm gonna make the card wide again so we
can see it a little better. So I'm just going back here and
make it wide, so this card is not really card-shaped.
Cards are not tall and thin like that and they certainly,
cards are definitely not like this card over here,
no cards look like that. That's ridiculous,
we don't want that. We want it to look more like a card, and
what makes a card look like a card? Well,
it's its aspect ratio. Right, the width, the relationship of
the width to the height, so we want to change that. So to do
that we can't have the edges pinned to the edges anymore.
So let's take our constraints to the pin it to the edges and
instead of making them pinned let's make them be greater
than or equal so that we, our card doesn't go off the edges
but it's not pinned to the edges either. So
how do we do that easily, or you can find out all
the constraints that are on a view by just selecting it and
going to this other inspector on the other side of your
attributes inspector, called the size inspector.
See here's my constraints, these are my four constraints.
So even as I mouse over them, look, they highlight.
So, right now they're all equals, they're pinned.
Okay, equal sixteen, pinned to the edge, equal sixteen.
You can change that equals just by editing them and
changing it to greater than.
We actually did this last time and we can do that for
all of ours. Just let them all just be advisory.
And let's not do the bottom right up against the bottom,
let's go ahead and just do greater than or equal to. And
same thing here, greater than or equal to and we'll do 16.
So it's at least the same on all sides.
So now, these constraints on the edge are just advisory.
They're just saying make sure you don't go past 16 points
from the edge. So that's great. But now,
the lines are all red, you see how everything's turned red?
That's because we no longer specify where this card's
supposed to be anymore. Since we're not pinning it to
the edges, where it's supposed to be. Well, let's first fix
this aspect ratio problem. Okay, I want the card to
have an aspect ratio, you know, kinda like the ad or so.
Basically, five across to eight down seems to be typical
card ratio. And it turns out you can
fix the ratio of a view by doing control drag.
But you don't control drag to another view like we do when
we're pinning to the edge. You control drag to itself.
When you control back to itself,
you're offered the option of fixing the width, the height,
or the aspect ratio of this view. So I'm gonna fix
the aspect ratio. So now, I've added a constraint,look
at it over here, that fixes the aspect ratio.
Now of course, I don't want aspect ratio to be 259 to 461.
So I'm gonna edit to make it five to eight.
So I fixed this after that.
This still doesn't say anything about where the thing
is supposed to be or what size it's supposed to be or
anything like that. So let's put another constraint that
says it's gonna be right in the middle.
So you see how I used the dash blue lines to drop it
perfectly in the middle? Now I'm gonna control drag from
the card back to my outer view right here. And this time,
instead of doing trailing in top which I already have those
greater than or equal to ones, I'm gonna pick center,
horizontally, and vertically. And you notice this says,
horizontally and vertically in safe area. So
every view knows it's safe area.
It's safe area is the place it can draw without overriding or
impinging upon other views space. So for this
orange view, it's safe area does not include this place
where the facial recognition and the time of day.
All that up here, so it wouldn't draw up there.
It also does not include this little bar down here.
If there were bar buttons along the bottom or a title
across the top, it wouldn't include that either and
that's all automatic and not only automatic, as it changes.
This constraints will automatically adjust to that.
So, if you put a title on the top of this view and
let's say very move down, then my card would move down to be
the center of the new safe area. So that's what safe
areas all about. We are always creating constraints between
view safe areas, all right? Okay, so now I've said where
it is but things are still red. Why are they still red?
Well, because I haven't said how big this view is.
I've said what it's aspect ratio is and where it is and
I've said that it can't go pass the edges but
I haven't said what size it is. A very small card would
satisfy all this constraints over here, right?
Very small card would be going out the edges. It could be
the right aspect of ratio, it could be the middle or larger
cards that doesn't go out to the edges, could fulfill all
these, all right? So, how do I tell the system, I want you to
be as big as possible and still satisfy this? Well,
I'm gonna do that by pinning. By dragging to myself,
my width. And I'm gonna set my width which is currently 259,
I'm gonna edit it. By the way, that fix the problem cuz now
look no red because I've set how high it is.
But I want it to be bigger. I'm gonna say I want it to be,
let's say 800 wide. Okay, now as soon as try to
have a constraint to say this is 800 wide.
Wow, we went red again. Now why are we red? We're red
now because these constraints can not be satisfied.
There is no way you can be 800 wide and also no
go off the edge. Basically, so that's the problem.
Now, how are we gonna fix this? Well all these
other constraints besides the width I got to have those.
If I don't have those edge constraints,
it could go off the edge, got to have it. Aspect ratio,
that's what I want card to look like, got to have it.
In the centre, I definitely want the card in the centre.
Width, well I wanted it to be 800 but
really I just wanted it to be big.
So, that 800 width is not as important to me,
in other words, it's lower priority constraint.
So, I can tell the system that by going over here and
editing this constraint, and changing it's priority.
You see priority 1,000 right there?
That's is the max priority, that is required priority.
So, we can pick any priority less than a 1,000 cuz all of
these are at 1,000. And this will be less important.
So we'll still try to satisfy it as best we can.
But it won't override any of the other ones.
We do that by clicking on the priority.
We could type a number, or
we can pick some kind of well known ones,
like high priority. And whoa, look what happened.
All the red went away, it made the thing as big as it could.
It's still satisfying all the constraints.
It's doing that both here and over here.
See, it made it as big as it could and still have that
five to eight aspect ratio in the middle. So,
that's the magic of constraint priorities, okay. Making
constraints that don't matter as much have lower priority.
So we'll try to give you as much of them as it can but
it will give in on those lower priority ones.
Everybody cool with that? Okay, so now we got this thing
looking more like a card. It's got a card aspect ratio.
So let's turn it back to clear, here. And go back to
drawing it, because we still have only done the corners and
we need to do the rest. So let's next to do the face in
the middle and of a face card, we need some kind of image.
I'm going to do that by drawing an image,
and I just happen to have over here, somewhere,
not this. This guy right here.
Face cards, a bunch of face card images. Woohoo, okay.
And I'm just gonna drag all these images into my project.
Well, where do I put them? That's what this
Assets.xeassets is for, the place where the icon was here.
You can drag any images you want in here.
So, I can go grab all of these images, drag them all in.
Now when I do that, it looks like some of them didn't come
in, these ones that say @2x.
You see, @2x? No, those didn't drag in. Yes, they did.
That @2x means it's the same as the one that doesn't have
@2x, but it's twice the resolution.
So it put them as a 2x version, twice resolution.
Now, some devices have three x resolution, like iPhone plus
for example. I don't have any cards in that resolution so
it'll fall back to using the 2x resolution.
But I probably should add 3x resolutions to all my cards.
Now these jpegs that I dragged in,
this is telling me the name of it. And it got it from
the file name of the jpeg, but you can rename these to be
whatever you want.
I've conveniently named them Rank suit. Okay? So that I can
find them. And putting these images in my face card is just
a matter in my draw(rect) of looking these up by name. So
let's go to our playing card view. Back to our draw rect
where we draw our roundedRect here. Now we're gonna say if.
We can let the facecardimage =, better
go wide here, = UIImage. So UIImage is a thing that
represents an image, and if you look at its constructors,
it has quite a few, but one of the ones it has is,
named. And now you just specify the name, and this
name has to match this name that's in xcassets over here.
Okay, so that is our rank string,
Plus our suit. Okay, so that is the name. So if we're able
to find that then we must have found a face card.
So now we're gonna just put that face card image.
Draw it and we draw by saying .draw In and
I'm gonna draw it in my bounds but actually I
don't really wanna draw that face card in my full bounds,
it might smash into the corners, right?
So I'm going to take that bounds and
zoom it down a little bit by one of my constants down here,
this constant right here, so
this is SizeRatio.FaceCardImageSizeR-
atio and I currently have it set to be 75%. So I'm gonna
have my face card be 75% of the full size right there.
And that's it, that's all you need to do to draw images.
Really easy to get them by name and
then just draw them in some rectangle. So let's go change
our card to be a face card, how about, let's say a Jack,
11 is a Jack. Make sure this draws and
it should be 75% of the size of card here.
There it is, it is and when we rotate it draws it smaller.
Cuz it's drawing it compared to our bounds,
which our bounds are changing when we rotate. So
that's super cool. What about pips? So what if we head
back to having the rank B5, then in the middle we draw
five hearts, five little hearts. Well, I'm not gonna
waste our lecture time going through code that does that,
because it's pretty straightforward code and
you're not gonna learn anything new.
You can certainly look at it offline,
I'll be posting this code online. So
I have it right here though, it's called drawPips. So
there's this function drawPips.
The way it works is data driven, so like for the five
rows goes two pips, and then one pip in the middle, and
then two pips at the bottom, right? Or an eight is two
two two and two, etc, so it's just data driven. And
it literally just does a for loop and goes through the for
loop and draws either one pip or two pips and
just goes down and draws however many rows there are.
It does have this kinda cool little embedded func,
you notice that functions can be inside functions in Swift.
This createPipString just creates an attributed centered
string, but it does't have the five. I's just the pip
part of it, but i's still centered which is nice so
it draws it in the center of the card. And it kinda picks
the size by guessing what the right size would be and
seeing how big that is and then adjusting it so
that it picks the perfect size pip to fix,
to fit the space that's available. So you can look and
see how I do that using center attributed string there. Okay,
that's pretty much it. So if it's not a face card, then we
want to drawPips, so let's see if that works for our five.
Looks pretty good and let's see, we'll rotate it, smaller,
it all got smaller. So easy to do this stuff, right?
Now we kinda are at a point with this thing,
there was one other thing, sorry,
we have to draw which is the back of our card, okay.
So it really should only do this stuff if it's face up,
all right. Should only do the face card in the pips if it's
face up and we already made it so that if it's not face up,
it hides our little labels, right?
It is hidden, hides our labels so that's good. But if our
card is face-down then we need to show the back of the card.
So I'm gonna do that with an image as well.
I'm gonna say if let cardBackImage = UIImage
again, named, and I'm gonna call it cardback.
So I'm gonna look for
an image named cardback and if I can find it, then I'm gonna
have draw in. And this time I'm gonna draw it in my entire
bounds because it's not gonna hit any corners,
the corners aren't there because I'm face down. So
I need an image named cardback. So I'm gonna over
here to assets and I have to put an image called cardback.
So I'm gonna grab this image right here,
it's my Stanford image. And I'm just gonna rename
it right here to cardback, so this is my cardback.
Notice it only has the lower resolution version there,
it didn't have an add time 2X but
I can drag higher resolution versions in to provide higher
resolutions just like that. And this one is so
high resolution, it's got a little tree in there even,
okay and that's perfectly fine. No law that says it has
to be just a scaled-up version of the same thing. So
now I have cardback there, so now, let's go and make our
card be face down by setting our isFaceUp here to be false.
Okay, and run, and we'll see the back side of our card.
And hopefully, we don't see any corners,
we don't see any face, we don't see any pips. We won't
see any of that stuff, we'll just have the back of our
card. And this is a high resolution device so
we got the 2X version. And you can see it's actually
kinda jaggy, we really could use a 3X version here,
it would be nice. Okay, now the next step if I were really
developing this is I would want to go up here to my rank
and suit and try every rank and every suit face up and
face down, and make sure this all worked.
Well, can you imagine if I had to do this. Okay, make a six
and then a clubs and run. No, okay so it's seven and run.
It would be tedious as all get out to be going back and
forth running. What would be awesome is, so I can just see
this playing card view right here in the interface builder.
And of course, I can do that, I wouldn't have mentioned it.
So let's go here, and how do you do that?
You just put @IBdesignable in front of your view.
If you put that in there,
then when you go to interface builder,
it will compile your view, put it in the environment and
put it here. Now, it's blank, why is it blank? Well,
it's actually blank because it's face down, and
images don't work with image named in interface builder.
For example, if I put this face up again,
you'll see that it works with the pips, because they don't
use any images. All right, go back to my storyboard.
Look, I got pips, I got my corner things too. Okay, so
it even does subviews. So what about those images?
How am I gonna do the images,
cuz that's problem not just for the card back, but
if I make it be a a face card,
the face card is made with images. And so
I'm getting the corners, but I'm not getting my image.
Well, it turns out there's another version of image named
that you can use, that will work with both. So
it will work image named when you run,
but it will also work with image named when you are,
when you're in interface builder environment.
And it looks the same, I can never even remember it myself,
so I had to write it down here. It's, in: Bundle
(for: self.classForCoder),
compatibleWith: traitCollection,
okay. I think I typed that right. So this is the extra
couple arguments you need, you put it on all your image names
if you want this stuff to work in interface builder.
So now if we go to Interface Builder, all right,
it's showing the image. But this is only half the battle
because, if I wanna look through all my cards and
make sure they're working, I still have to go back here and
change these ranks and suit and
then go back and see it again. And what would be really cool
is if I could bring up the inspector, click on my card,
and instead of just seeing view attributes, if I could
see rank and suit and his face up, wouldn't that be awesome.
If I could just extend this The inspector, well,
of course we can do that too. All we have to do is put
@IBInspectable in front of any var that we want to
be inspectable in Interface Builder. So I'm gonna put it
on all my vars, I'll make them all be inspectable.
The only trick here is that you have to explicitly type
any IBInspectable, you cannot let this be inferred by Swift.
Because while Swift is good at doing inference,
Interface Builder not so much, not quite so good.
All right, so here we go. Now if I click on my view,
look at this, rank, I could try 5.
I could try 12, all right? I can try 2,
I can go even just go all through my cards, like this.
And since I've represented my suit as a string,
I could even have X be my suit right there.
That works? Okay, so that's it for all the drawing stuff.
Let's go back now and learn a little bit about multi-touch.
So I'm gonna go back to our slides here. And we're running
a little late, so I'm going to zoom through these. All right,
so we've seen how to draw, now how do we get multi-touch?
How do we get all these gestures and
stuff people can make with their fingers on the screen?
And you could get get all the touch events yourself,
that's legal. You could, and look at them,
look at every finger when it moves,
but that'd be incredibly tedious, so we don't do that.
Instead, we let iOS look at all those little movements and
turn them into gestures, like swipe, pinch, pan,
tap. So that's the level at which we program this stuff.
Okay, now gestures all represented in iOS
with this class UIGestureRecognizer.
It's a thing that recognizes a gesture from all those finger
movements. All right, that class is abstract, okay,
it itself doesn't know how to recognize any gestures. But
there's a lot of subclasses of it that know how to recognize
various gestures. So when you're recognizing a gesture,
there's actually two parts to it. One is, you have to tell
a view, please start recognizing pinches,
please start recognizing taps. Then you have to
provide a handler so that when it does recognize it,
it calls some function, so there's two parts.
The first thing, asking a view to recognize a gesture,
is surprisingly often done by the controller, or
in your storyboard. That's how you add gestures, usually.
Sometimes a view will add a gesture recognizer to itself,
if it's just totally inherent to what it does.
Like a scroll view will add pinching and
panning gestures to itself,
cuz it's not even a scroll view without those gestures.
But a lot of times, it's the control that does it.
The second thing, the handling of the gesture,
if it something that affects the model,
then the controller is going to handle it.
If it's something that only affects the way things is
viewed, then the view will often handle it directly. So
we'll see examples of both of those in our little demo.
So, the first part, how do you add a gesture to a view?
How do you tell that view, start recognizing this?
Well, usually we do this in the didSet of an outlet
setter. So here I've got an outlet to some view that I
want to recognize pans. Okay, it's some view, and I want it
to recognize pan gestures. So in the didSet of the outlet,
remember this didSet is called when iOS wires up that outlet
to the view that you want to pan. Then I'm going to
create a concrete instance of UIGestureRecognizer called
a UIPanGestureRecognizer. Now all of the recognizers
have the same initializer. It has two arguments, the target,
that's the object that is going to handle this,
it's usually either the controller or the view itself.
And then it has the action, and that's just the name of
the method with #selector around it. You see that
#selector in yellow there? That is going to be called
when this gesture starts to recognize a pan happening.
So then, once we've created a UIPanGestureRecognizer,
we ask the view,
please start recognizing this. And we do that by calling
addGestureRecognizer. And a view can have as many gesture
recognizers as you want.
It could be recognizing 20 different gestures at the same
time, it's perfectly fine.
All right, so now let's talk about the handler. So
when a pan starts to happen, a handler's gonna get called.
And the handler's gonna be that pan method that we saw
over there. And inside that method, we're going to have
to be able to get information about the pan.
Well, each kind of gesture has it's own information.
Like a pinch gesture has the scale you're pinching to,
a pan gesture is where is the pan happening. So if you
look at UIPanGestureRecognizer in the dock,
you'll see it has methods like translationInView.
That tells you where the pan is in that view.
Or velocity, how fast is the pan happening right now?
Or even setTranslation, which let's you reset
that translation in view, so you get incremental panning.
Instead of the continuous length of how far you've
panned since the start of the pan,
you get how much you got since the last time the pan moved.
Okay, which can sometimes be useful. Now,
the abstract superclass UIGestureRecognizer,
it also has a very important var called state. So
this whole gesture recognizer thing is a state machine, and
this state var represents that.
So as soon as a gesture becomes possible,
like a pan. Probably a finger touches down,
now it's possible.
And then as soon as it moves, it moves into the began state,
okay, so this pan has begun. And then as the finger moves,
it stays in the changed state. But it really keeps moving
to the changed state from the changed state over and
over. Now every time one of these state changes happens,
that handler gets called. Whoever's handling this thing
gets a chance to do it. So for a pan gesture,
you get .changed called every time the thing moves.
And then eventually the finger goes up,
and it ended, and you get .ended. So your handler's just
called every time the state machine changes.
Now, some things, like a swipe, are discrete, either
the swipe happened or it didn't. You don't get .changed
as your finger's flying across the screen, it's a discreet
gesture. You just get .ended, or for a swipe,
.recognised gets sent to your handler once, and that's it.
But for continuous gestures, you get the .changed.
Now, there's also two other interesting states, .failed,
and .cancelled. So .failed can happen when you have multiple
gestures, and one of them wins.
Like let's say you have, I don't know, a tap gesture and
a pan gesture. Well, as soon as you go mouse down,
it could be either of them.
But as soon as it doesn't it come right back up as soon
as you touch down. Soon as you come back up, it's like,
it can't be a pan anymore, so that one's cancelled,
cuz It failed, basically. So it can go into failed states,
but that's only if it actually starts up. It wouldn't be
recognized in the first place if it didn't get that far.
And then so cancelled is another one that's
interesting. And this happens a lot with drag and drop.
Which is, you started something, and it started up,
and it's going good. But then a drag and drop happens, and
now it's canceled. Whatever gesture you were recognizing.
So you do wanna look for failed and canceled, and
make sure you clean up or whatever. Take away something
off the screen or whatever, because your gesture has
failed, or has been cancelled by something else. All right,
so given this information, what would our pan handler,
the handler for the pan look like?
Okay, so it's just pan with the argument being
the pan gesture recognizer itself handed back to us. And
we switch on the state, we always switch on the state.
And if it's changed or ended, and
notice I'm using fallthrough there, but I could have just
said .changed, .ended there. So if it's changed or ended,
my pan is still moving, or I've just finished it.
Then I'm gonna find out where the pan was by calling
translationin: view on the recognizer.
Then I'm gonna do something based on where the pan went.
And maybe if I'm looking for
incremental pans, I'll reset it back to zero. So
that the next one will be from zero and be incremental. So
that's it, simple to do these handlers. Now what are some of
the concrete handlers besides PanGesture?
Well there's PinchGesture. Its information is the scale. So
if I start here with a pinch, and I go twice as wide,
well that's scale 2.0. Or if I start here and
go half as wide, it's 0.5. And there's also velocity for that
one. There's RotationGesture, which is like turning a knob.
A two-finger gesture turning the knob.
And in radians, it'll tell you how much the knob has been
turned in radians. There's a SwipeGesture, and you can,
now swipe is a little different than these other
ones in that you configure the swipe. How many fingers?
What direction, left, right, up, down?
And then you turn the swipe gesture on by adding it. And
then when the swipe happens, you'll just get .ended, your
handler will get called with .ended. So it's just, there's
no, it's different in that you configure it up front and
then it just tells you whether it recognized it or not.
There's TapGesture,
which feels like it would be like swipe,
a discrete gesture, but actually,
since it does double tap and other things, you're always
looking for .ended only with the TapGesture, usually.
But you also configure it like a swipe gesture how many taps,
how many fingers etc. There's also long press. Long
press is you hold your finger down on the screen for enough
time and it starts recognizing it. This is surprisingly
a continuous gesture, because as you're holding it down your
finger might be moving a little bit and
that's okay it's not a pan. Okay,
cuz it can only move a little bit. But
if it does move a little, you'll get .changed.
And you can configure how much movement you allow and
how long it has to be pressed before it's a LongPress.
This one gets interrupted a lot by drag and drop.
Because drag and drop uses LongPress. That's how you pick
something up with drag and drop is LongPress.
So if you have a LongPress, And there's some drag and
drop going on, you know the system is very smart about
figuring which one you actually intend. But
it could cause your long press to be cancelled.
All right, so let's see all this in action with a demo,
we only have five minutes left, but I think we can do it
in five or ten minutes. We're gonna add three gestures
to our playing card. Were gonna add a swipe,
which is gonna flip though our deck of cards. So that's gonna
affect our model. Our model is that deck of cards, so
that's something our controller is gonna have to
do. Then we're gonna have tap will turn the card over. We're
gonna do tap by adding the gesture in the story board,
not even in code. And then we're gonna have pinch which
I'm gonna use to resize the face card faces. And
that's the view only thing, so the handler for
that will be in the view. And since I won't be back to
the slides on Friday, no section again,
Homecoming week. This time we have conflicting schedules, so
we couldn't do structured section this week,
unfortunately. Next week we'll start doing multiple MVCs,
View Controller Life Cycle, and hopefully we'll get into
animation as well next week. All right, so here we are,
let's make our thing look a little better.
Let's go get back and get a nice, nicer thing,
maybe clubs this time.
And go back here so that x will have our clubs. Okay,
so we have nice looking cards. And, let's do the swipe first.
So the swipe, to do the swipe let me get both our
controller and our view up on the screen at the same time.
So here's our controller. It just has a deck of cards,
it doesn't really do anything. wanna add a gesture to this
playing card view that is swipe. I need an outlet to it.
My controller can't talk to that thing with an outlet.
So I'm just gonna control drag like I would drag anything to
make an outlet. Click it here, it's gonna be an outlet.
It's gonna be my playingCardView is the outlet.
Here it is. When this gets wired up,
I'm going to immediately add adjuster recognizer.
So I'm gonna do that in the didSet of this, so that when
iOS sets it I get to execute my code. I'm gonna do a swipe.
So, I'm going to create a swipe gesture,
UISwipeGestureRecognizer.
And the constructor is this target action thing.
Since swipe is going to flip through the cards, it's going
to affect the model. So it has to be handled by me,
the controller. Okay, so self is the target. The view can't
touch the model, so there's no way it could do the swipe. And
then the selector can just be any function. So,
I'm gonna have a function here called nextCard, which goes to
the next card. It's not even gonna have any arguments.
That's gonna be the action I want to be called when a swipe
happens. So, I just say #selector and
then I gave the name of it.
Next card, it has no arguments but if it did I would just put
the args in there. But it doesn't have any arguments so
we don't need that. Selector(nextCard). So
that's my swipe gesture. Now we need to configure the swipe
gesture. So for example I can set its direction.
I could say it swipes to the left for example. Swipes to
the right you could even say, swipes to the left or right.
Could put a little array notation there, for
left and right. So now I've got my swipe,
it's gonna be a single, what have we got?
Yeah, so this is an error right here. I'm gonna click
on it. It's gonna cause our screen to get all wonky here,
so let's move it around. Let's look at this error right here.
It says, the argument of #selector refers
to an instance method nextCard(), which it does.
That is not exposed to Objective-C. My gosh,
this whole mechanism is built on Objective-C, mechanism of
target action. So any method that is going to be the action
of a gesture recognizer has to be marked @objc. That exports
this method out of Swift into the Objective C run-time
which underlies the running of the iOS. Even with Swift code,
still got the Objective-C run-time. Okay, so that's what
that's all about. This always has to be ,just mark it objc,
It's not a big of a deal, just got to mark it. All right,
let's go back to our split screen here. This and this,
rearrange everything. Back to automatic.
All right, so now that we have this SwipeGestureRecognizer,
we need to ask this playingCardView,
please start recognizing it. So we say playingCardView,
add this GestureRecognizer(swipe).
And now it will start recognizing it.
And that's all we need to do. Now this next card
is the thing that's gonna flip through our cards. So
how do we implement that? I'm just gonna say if I can get
a card out of my deck. Because my deck might be empty.
That's why I have to do if let there.
Then I need to set the playing card view's rank
equal to something. And I need to set the playing card view's
suit equal to something. Now here's where the controller's
doing its job of converting between the two. So
we're going to convert by saying the card's.rank,
luckily we have order which does the card's order, and
card.suit has its raw value. Okay,
so this is just converting between the model and
the view there. Everybody got that? So
let's give it a try, see if this works.
So this should swipe through random cards by doing swipes.
So here we go we go, swipe, sure enough, look at that.
Swiping through. So that was really easy, right? Just have
that deck. All we had to do is just set the playing card view
to show a different card each time. All right,
the next thing we're gonna do is tap to flip the card over.
So tap, I'm not even gonna do this code right here.
Instead I'm gonna go over here, and
grab a tap gesture from, for this view, from here.
It's down towards the bottom. Look at all these gestures,
pinches, rotations, swipes. Here's tap, and
I'm gonna drag it to the view I want to recognize a tap.
Which is my playing card view. I drop it, and it shows up,
if we zoom in you can see it, right up in this title bar up
here. You see that right there, Tap Gesture?
You can click on it and inspect it.
Right, how many taps? How many touches?
You can also control drag from it to set an Action. So
I'm gonna set an Action here. I'm gonna call it flipCard,
cuz that's what I want it to do, flip the card.
I want to fix that anything. Just like any Action,
I want it to fix the argument. So here's my flip card.
And inside flip card here, I'm just gonna say
playingCardView.isFaceUp = not playingCardView.isFaceUp.
Okay, I'm just gonna flip the card over, and that's it.
So some gestures are really easy to write. And actually,
I abbreviated that a little bit. And now if I click, you
see how it's flipping it over. Okay, now I know we're rushed,
but actually I'm going to do the right thing here.
This really shouldn't be like this. I should switch
on the sender, which is the recognizer's state, and
make sure that we are in the ended case to do this.
Now, it'll usually work to not do that,
but I don't wanna show you something that's
really kind of not correct. Okay, and then the last
one we're gonna do is pinching to set the size of the face
card. Well, to do that, I need to go back to my view count,
my view, my custom view over here. And
I need to make it possible to change that, So right now,
actually, let's go here. Okay, view.
Okay, so right now the size of my face card, remember that's
a constant. This SizeRatio.faceCardImageSizeTo-
BoundsSize, so I'm gonna change that to be a var.
I'm gonna call it faceCardScale. Okay,
so I need to create a new var to do that. So let's go up,
do it all at the top. So we can easily see it here,
var faceCardScale. It's going to be a CGFloat.
I'll set it equal to that constant. Don't forget to do
this. Okay, although we don't really need
setNeedsLayout because changing the card size,
the faceCard does not affect the corners, okay. So
I don't need to relayout. So I've got that faceCardScale,
so now I'm gonna create a little func that is going to
be a handler for a pinch gesture.
Okay, I'm gonna call it, adjust, I had a good name for
here so it's easy to understand what it is.
What did I call this thing? adjustFaceCardScale(byHandlin-
gGestureRecodnizedBy recognizer: UIPinch),
now, this is an intentionally long name there.
So that you'd understand that this is the handler for
the gesture. And since it's a handler, it needs to be @objc,
of course. And, inside here, I'm just gonna switch on
the recognizer's state, as I always do.
That's what we do in standard in these handlers. And
if it's changed, so the pinch has changed or
if it's ended, then I'm going to set my faceCardScale,
this thing I just created up here, okay,
to be*= recognizer.scale.
Now, I only want incremental changes because I'm changing
the scale each time. So, otherwise, it would
just start to be exponential. So I'm gonna reset
the recognizer's scale to 1.0 each time that this happens.
And then we're gonna ignore all other states of the state
machine. We don't care when it began and all that, stuff. So
now we're gonna have this adjustFaceCardScale(byHandlin-
gGesture recognizer) be added back in our controller as
a pinch gesture. So here I'm gonna create a pinch gesture.
Let pinch = UIPinchGestureRecognizer,
same target inaction thing as the other one, but
this time the target is going to be the playingCardView.
It's gonna handle this directly.
It's not gonna go to the controller, and
the selector is that method we had over there.
Okay, it's in our view, and I'm gonna call it pinch. Okay,
and now I just need to tell the playingCardView to add
this gesture recognizer pinch, and it will start recognizing.
Okay, so let's take a look. Oops,
what did I do wrong here? What does it say?
Unresolved, okay, let's use scape completion here, adjust,
sorry, PlayingCardView. I need to say that it's in
PlayingCardView. That's the problem there, handler.
Sorry about that. Okay, so let's find a face card.
Here it is. How do you pinch in the simulator?
You hold down Option, you get these grey things, and
when you mouse down, you get to pinch. So
see how that's only effecting the view?
It's not effecting anything else, effects all the cards
And that's it. Okay, sorry to rush that at the end. You'll
be doing all this stuff in your assignment number three,
which just went out. It's due in a week,
in other words before lecture next Wednesday. And
I will see you all then,
actually, I'll see you on Monday.
And if you have questions,
I'm here, as always. >> For
more, please visit us at stanford.edu.
Không có nhận xét nào:
Đăng nhận xét