Stanford - Developing iOS 11 Apps with Swift - 6. Multitouch

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[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.
Info
Channel: CS193P
Views: 17,196
Rating: undefined out of 5
Keywords: iOS, Swift, Stanford, CS 193P
Id: _ao1tlshRi0
Channel Id: undefined
Length: 85min 23sec (5123 seconds)
Published: Mon Jan 01 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.