[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.