Learning Swift #6 - Making a Drawing App
Hey guys, this is Mat Kids11, and today I'm going to be making a drawing app in Swift.
So the main thing I plan to learn in this video is how to use the Core Graphics APIs that I'm used to in Objective C, uh, and how to use them from Swift.
So I'm just going to go right ahead and create a new single view application, and I will call it Bad Drawing. I will use Swift as the language. Create it. I'm going to delete the unit tests, and I'll get right ahead to making the UI.
Now, the UI is going to be dead simple, I think. At first, all we're going to have is a clear button to clear what the user has drawn, and then the user will be able to draw on this view, uh, in order to basically, uh, you know, draw lines with their finger. So they'll be able to lift up their finger, draw a little line, lift it up, draw another line, then whatever. When they hit clear, it'll erase everything they've drawn.
After this, I think I might make a button to set the color to either red or black or something, just so we can experiment with different drawing colors. But at first, I think we're just going to be drawing in one color.
So the first thing I'm going to do is create a new class, and I'll make it a Cocoa class. I'm going to make it a UIView. I'll call it DrawView; that sounds fine. What I'm going to do is I'm going to create a DrawView on here. So I'm going to drag in a view, and you can see it's kind of hard to see because it has a white background. But I'm going to set its class, and I can do that here to DrawView.
So this way, it will essentially create a DrawView on this view controller that, uh, now we can write code for. In the DrawView, we're going to have stuff that detects when they touch it, and we're going to draw the lines and everything, and there will be a way to clear it.
But before I set all that up, I'm just going to set up an IB action just in our main view controller, um, for clear tapped. So let's go ahead and set this touch up inside. We'll go all the way to clear ta.
Um, so now let's just see what happens when we run it. Already it's complaining. I want this to be a UIView or UIKit. I don't know why; maybe I accidentally created a Cocoa class and not a Cocoa Touch class.
Um, when I run it right now, let's see what happens. It works. Um, so that's good. So we're going to need an initializer, um, for this. I think actually we don't even need an initializer for this.
Um, if we did need an initializer, we would either use init with coder or init with frame. I think for a view that we have set up in Interface Builder, we would use init with coder.
So let me just give you an example. Um, you do something like this: super.init(coder: decoder), and then you would do your initialization code here. So for instance, I could set our background color to black, and this will just be—whoops, I need to name this argument. This will just be, uh, to demonstrate that this initializer is getting called. You can see it set the background color to black in our DrawView.
So this is how you would do initialization code for your view if we wanted to.
But anyway, what we're going to do to make it so that the user can draw on this view is actually kind of simple. When the user first puts down their finger, we call that point, uh, the last point they tapped, which is true; it's the last point they tapped.
Then let's say they move their finger, and we get a touch as moved event. We now know where their finger is, and we know where the last place their finger was. So we draw a line between those two points, and then we update the last point to be the point they just moved their finger to.
So that way, when they move their finger again, we draw a line between the last point and the new point that they move their finger to. So we're going to draw what looks like it's tracing out the path that they draw with their finger, but it's really just figuring out each point their finger passes over and drawing lines between the points.
Um, so that's not going to be too difficult, but we are going to need an array of points, of lines actually, an array of lines. Then we'll need to figure out how to draw that array of lines.
Uh, so let's just go ahead, and I will, uh, I'll create a new class called Line so that we can declare our array of lines. And here we go; here's why it didn't work before. It's because I was under OS X and not iOS.
So I'm going to create a new Swift file, and I'll call it Line, and I'm going to declare our class here. Line and start will be a CGPoint and end will be a CGPoint.
And we will initialize it. So this is just the initializer. I use underscore start so that this doesn't have the same name as this, and it's complaining because I need to import UIKit and not just Foundation.
Okay, so this is our Line class so far. And now I can declare our array of lines. So I'm going to say lines will be of type Line array, and I'll start it off empty.
And I'm also going to declare our last point. It will be a CGPoint exclamation point. And here's already the feature that I have come to really appreciate about Swift that I really, uh, couldn't stand in Objective C.
In Objective C, if I wanted to have a CGPoint that was like a property or a variable or something, I couldn't set it to nil, right? I couldn't set it to null. I couldn't tell if there was no point there, and I would just have to initialize it with maybe 0, 0, or something like that.
Um, in Swift, just with this nice wrapper type with this exclamation point, it doesn't always have to have a value, even though it's a simple C structure. So that is something I really appreciate about this language: these nice wrappers.
Uh, but anyway, let's go ahead, and we're going to implement, uh, touches began and touches moved because these are how we're going to get, uh, touch input from our user.
So, touches moved. Uh, so touches began, all we're going to do is we want to set the last point to be the point they just touched with their finger. So I'm going to do lastPoint equals touches.anyObject.location(in: self), and all this does is it will assign, uh, the last point to be the point they touched.
This is how you get the point, um, that they touched in a touch event.
So now I'm going to go here, and I'm going to say newPoint. So when they move their finger, I get the point that they moved their finger to. And now, uh, I'm going to add that line between the last point and the new point to our lines array.
So to do this, all I have to do is something like this. So it's going to start at lastPoint; it's going to end at newPoint. And this is how I add the line to the array. And the last step is to set the last point to be the new point.
And this way, next time we draw, we move our finger again, the line will be drawn between the point here and the new point that they move their finger to.
So if that makes any sense, we don't want to have it so they touch the first point, and then whenever they move their finger, no matter which point they moved it to, it would draw a line to that point. That would look ridiculous.
And actually, maybe I'll show you what happens if we comment out this. Um, I'm going to show you that after I finish making this work.
So the last thing we need to do is actually draw all of our lines. We have a lines array; it's going to get updated, uh, but we need to actually draw all the lines on the screen.
And this is the part that I didn't actually—I'm not entirely sure how it'll work in Swift. I have some theories, but I don't know really how Swift interfaces with something that is a C API.
Um, Core Graphics is a C API. Core Graphics is what we're going to be using to draw. So, uh, the function we override is actually pretty predictable; it's called drawRect.
And drawRect will get called when it's time for our view to redraw itself. So what we need to do first is get access to the graphics context, which is basically—if you're an artist, the graphics context is the, uh, canvas you're about to draw on.
So the way we get the context—and this is how it worked in Objective C, I guess it's—oh yeah, it's the same in, uh, in Swift. So we use UIGraphicsGetCurrentContext, and that will give us the context, and the type of this is CGContext exclamation point.
Um, so that's nifty. So we get our context; that's very similar to how it is in Objective C. And now we need to draw onto our context all of these lines.
So to do this, uh, we basically tell the context we're about to draw a path, then we give it the points in the path, and then we tell it what color to make the path, and then we say stroke the path.
And stroking is equivalent to brushing over the path. You can also fill a path, so if the path is a circle, it'll fill the circle, uh, but in this case, we want to stroke the path, and maybe I'll show you what filling it looks like as well.
But I said a whole lot; let me show you how it works. So, uh, it looks like this is exactly the same way it actually worked in, uh, in Objective C when you were using the C API.
We begin the path, and we give it a context. So these are a whole bunch of functions that take the context as the first argument.
Um, so we begin the path, and then I'm going to say for line in lines. This will go in order through every line in the lines array and it'll put the line in this variable line.
So that was a mouthful, but it's actually pretty simple; we're just iterating through all the lines and getting each one.
So to draw a line, the first thing we're going to do is CGContextMoveToPoint, and we can move to our line.start.x and line.start.y. And then we add a line to point line.end.x and line.end.y.
And basically for each line object, we're just going to be moving to the start of it and then adding a line to the end of it.
So, um, this is just how Core Graphics works; it's kind of arbitrary how it works, um, but this is how it is, and it's pretty simple. So that'll stroke each line.
Now we need to, um, set the stroke color. I'm going to use yellow; this is just—or, yeah, I'm going to use yellow. This is just an RGB value, and now we'll say strokePath.
So we begin the path, we add the lines to the path, we set the path color, and then we stroke it. I could set the path color, I think, just about anywhere, um, but you have to stroke it after you're all done and have to begin, uh, before you start anything else.
So this is how we're going to draw. And the last thing to do is whenever they move their finger, so whenever a new line is added, we need to say self.setNeedsDisplay.
And this will cause the graphics stack to redraw our view and call this method again, and it'll draw everything over. So this actually should be pretty good. I'm not sure if there's anything I'm missing, but we'll find out in just a second.
So let's go ahead and run it, and it's kind of hard to see because it's thin, but it is drawing in yellow. So how about I make it thicker? Set line width.
Um, this will set the width to five, five pixels, and there you can see it. Let me make it black so it's more visually obvious that it's there. So I'm just going to change the RGB code, and there it is.
You can see it's kind of hideous the way it's working right now because it's drawing lines between each point, and each line has a, uh, a flat ending, so there's all these weird gaps.
I may be able to do CGContextSetLineCap. And so here's an enum: kCGLineCapRound. So this is interesting—it's a C API.
Um, this Core Graphics is a C API; it's from the C programming language. So, um, they, instead of using a regular enum, they just decided to have a wrapper around an integer and declare them like old-school enums in Objective C, or, uh.
But that's kind of bizarre; I was not expecting that. I was expecting a normal Swifty kind of enum. But anyway, now you can see it's much smoother.
What this does is it basically just makes each line have a rounded end, so when you draw a lot of lines connected, uh, it looks a lot smoother and cleaner.
Now we still haven't made the clear button work, and all we need to do to make the clear button work is erase everything in here in this lines of red and then redraw.
So I'm just going to go into ViewController because that's where it is, and somehow we need a way for this view controller to access our, uh, drawing view.
So I'm going to do that. I'm going to say var drawView; its type will be DrawView, and I need to do IB Outlet, I think.
And let's go here and we'll say, and why didn't that work? DrawView, huh? This is a DrawView, and this is an IB Outlet DrawView. Maybe I don't need to have this. I did this before, so I should just be able to set this outlet.
Huh, new referencing outlet? Can I do this? No. Let's see how I did it in my Bad Calculator; that was the first thing I made. And I believe I did do some IB outlets.
So let's open this up, and yeah, I just did it like that and I said IB Outlet and I had the type and, uh, huh.
What if I make this AnyObject? Will I be able to do it now? Yeah, okay. So that's weird; for some reason, when I have a custom view here, and it did have the right class, I just couldn't link it up with this.
Well, I used AnyObject. This seems like a workaround, but I think it's a bug in Interface Builder or something.
So I'm going to say, um, I'm going to do a type cast to make sure it's a DrawView.
Um, so this is so that I can actually get suggestions on what methods there are. So I'm going to do lines equals; I'm going to reassign it to an empty array, and then I'm going to redraw or setNeedsDisplay.
So this is all I need to do to clear it; I just empty the array of lines and then I redraw the view. And I'm going to run it; clear, and it goes away. Clear, and it goes away. So that works, and that's great.
So now one thing I want to do before we add color is I want to make custom accessors. This is just for show; um, I think it'll be cool.
So what I'm going to do is, like this, I'm going to have a get, and I will return start.x, and I'll have a start.y, and I'll return start.y, and I will have an end.x and an end.y.
This is actually not the best thing to actually be doing, but it's just a demonstration of how I could utilize, um, these properties.
So because I made these, now I can go back here, and in DrawView, instead of doing line.start.x, I can do line.start.x, line.end.x, and it looks like they're just normal properties.
And I want y there and y there. But since I have special accessors, it'll actually still work just the same way.
So this is nifty; I like the custom accessors more. I think this looks cleaner, especially because, um, it seems more natural for these C functions.
But that was just to demonstrate that I could do these getters and one possible application of them is this.
So that's cool. Now I'm going to add a button, and I will make it a red button, and I will add another button, and I will make it a black button.
And I typed a space at the end of the title by accident, so we're going to have these two buttons set the stroke color that they draw with.
So I'm going to go back to our DrawView, and I'm going to—or I'm going to even go back further to our ViewController, and I'm going to add two IB actions. I'm going to add an IB action function, and maybe I'll just make one, like I like to do, so colorTapped.
And it's going to say button will be a UIButton exclamation point, and this way I can only have—I only have to have one action. So here, touch up inside; we'll go to our colorTapped, and same thing here, colorTapped.
And let's go back. So here we're going to say if button.titleLabel.text equals red else if button.titleLabel.text equals black.
And this way I can, uh, get a different color. So I'm going to say color, and it'll start off being a UIColor exclamation point.
And say color equals UIColor.red, and here I'll say color equals UIColor.black. And otherwise, I will, uh, well, I would like to display some sort of error.
I don't really know why this would possibly happen, so I'm not even going to do anything here. I'm just going to try to use color, and, uh, if it's nil, the runtime will crash anyway, so it'll actually handle the fatal error for us.
So this is just to get which color they tapped on. Now I need a way to set the color here, so I'm going to go back to DrawView, and I'm going to have a drawColor, and that's going to start off with UIColor.black.
I think so we're going to set drawColor to be the color. So we're going to say drawView.drawColor equals color, and drawView is an AnyObject, so it's not going to let us set this property, I don't think.
And the only reason it's in AnyObject is because I couldn't figure out how to use Interface Builder properly, as you recall.
So, uh, I have to typecast it, but I know it's going to be the right type. Um, and actually since I'm using a cast, I don't really think I need to declare the type explicit here because it knows what type it is from this.
Uh, anyway, so I can set the drawColor, and now we have to actually use that. So in our DrawView, whenever they move, I'm going to add a color attribute to our line.
So there we go, and UIColor needs to write a capital C. So here we go; our DrawView will now give it a color, and I'll give it drawColor.
So each line now has a color. I'm still not doing anything with that because, uh, you can't really—when you're drawing with the CG path, you only set one RGB stroke color, and it strokes the entire path as far as I know.
So, uh, what we're going to have to do is, well, this is probably not the best way to do it, but I'm just going to do it anyway for show.
I can do this at the beginning because it's a global property, and I can do this as well at the beginning, but everything else I'm going to do in here.
And so this is—this is the way we were setting the color before; we were using setRGBStrokeColor. The problem with that is we don't have an RGB; we have a UIColor.
So in Core Graphics, there was a CGContextSetColor. There was a setColor with color, but that doesn't seem—setStrokeColor. Oh no, yes; setStrokeColor with color, that's what I want.
So that still exists; so it takes a CGColor, not a UIColor. So I don't know if we can do it like that. I guess that still works.
This is how it was also in Objective C. So for each line now, we're going to draw a separate path. We're going to begin the path, we're going to draw the line, we're going to set the color to be the line's color, and I'm not going to use drawColor; I'm going to use line.color.
CVColor—that was my bad. Then we'll stroke it, and before we do any of this, we set these two properties; we set the width and the cap.
These don't need to change each time in the loop, so we can just set them once outside of the loop.
And now, uh, well, I don't actually know if it'll work because who knows? We just did a lot. I can do that; I tap red, and voila, it's drawing in red.
This is—that actually worked much better than I expected!
Um, so great, and of course, clear still works; gets rid of everything. So this is how we use a little bit of Core Graphics in Swift. It seems basically the same way it was in Objective C.
So it's neat to see that they took all these, um, C methods and made them accessible from Swift code. I actually wonder if I can write regular C code and just immediately access it from Swift—that would be kind of cool.
So, uh, I don't know about that. I'll have to look into that, and there are lots of things I'm not sure how I would do with pointers and various other things.
But this is certainly, uh, a pleasant surprise to see how easy it is to use Core Graphics and how similar it is to Objective C.
Um, and I like the way I was able to use custom accessors, even though it's ugly, and it takes up a lot of space, and I probably shouldn't have done that, but it's still cool to have that power.
And, uh, yeah, so this was pretty much—it went according to plan; we made our drawing app, and it worked.
So thanks for watching, Mat's 101. Subscribe, and goodbye!