30 September 2010

Core Graphics 101: Lines, Rectangles, and Gradients

If you're new here, you may want to subscribe to my RSS feed or follow me on Twitter. Thanks for visiting!

 

Welcome to Core Graphics 101!

Welcome to Core Graphics 101!

Core Graphics is a really cool API on iOS. As a developer, you can use it to customize your UI with some really neat effects – often without even having to get an artist involved!

But to many iOS developers, Core Graphics can be somewhat intimidating at first, since it’s a large API, and has plenty of snags to get caught in along the way.

So in this tutorial series, we’re going to take the mystery out of Core Graphics and present it step by step in a series of practical exercises – starting by beautifying a table view with Core Graphics!

The table view we’ll be making will look like the above screenshot. The inspiration for this particular design cames from Bills, a beautifully designed app by PoweryBase. It’s a pretty cool app, check it out!

In this this first article of the series we’ll work on making a beautiful table view cell with Core Graphics. We’ll cover how to get started with Core Graphics, how to fill and stroke rectangles, how to draw gradients, and how to deal with 1 pixel-wide line issues.

In future articles in the series, we’ll work on beautifying the rest of the app – the table view header, footer, and finishing touches.

So let’s get started and have some fun with Core Graphics!

Getting Started

To get started, let’s set up a project with the skeleton of the table view we want to customize.

Open up XCode, choose the Navigation-based Application template, and name your project “CoolTable.” Compile and run the project just to make sure that a blank table view comes up at start:

Blank Table View

Ok so let’s add some sample data to this table view. Open up RootViewController.h and make the following changes:

// Inside @interface
NSMutableArray *_thingsToLearn;
NSMutableArray *_thingsLearned;
 
// After
@property (copy) NSMutableArray *thingsToLearn;
@property (copy) NSMutableArray *thingsLearned;

All we did here is add two arrays, which we’re going to add strings to to represent what to include in the two sections of our table.

Now switch to RootViewController.m and make the following changes:

// After @implementation
@synthesize thingsToLearn = _thingsToLearn;
@synthesize thingsLearned = _thingsLearned;
 
// Uncomment viewDidLoad and add the following:
self.title = @"Core Graphics 101";
self.thingsToLearn = [NSMutableArray arrayWithObjects:@"Drawing Rects", 
    @"Drawing Gradients", @"Drawing Arcs", nil];
self.thingsLearned = [NSMutableArray arrayWithObjects:@"Table Views", 
    @"UIKit", @"Objective-C", nil];
 
// Uncomment shouldAutorotateToInterfaceOrientation and change the return statement to the following:
return YES;
 
// Change the return value of numberOfSectionsInTableView to:
return 2;
 
// Change the return value of tableView:numberOfRowsInSection to:
if (section == 0) {
    return _thingsToLearn.count;
} else {
    return _thingsLearned.count;
}
 
// Inside tableView:cellForRowAtIndexPath, after the comment "Configure the cell":
NSString *entry;
if (indexPath.section == 0) {
    entry = [_thingsToLearn objectAtIndex:indexPath.row];
} else {
    entry = [_thingsLearned objectAtIndex:indexPath.row];
}        
cell.textLabel.text = entry;
 
// Inside viewDidUnload
self.thingsToLearn = nil;
self.thingsLearned = nil;
 
// Inside dealloc
[_thingsToLearn release];
_thingsToLearn = nil;
[_thingsLearned release];
_thingsLearned = nil;
 
// Add the following new method
-(NSString *) tableView:(UITableView *)tableView 
    titleForHeaderInSection:(NSInteger)section {
    if (section == 0) {
        return @"Things We'll Learn";
    } else {
        return @"Things Already Covered";
    }
}

Ok great – now we have some sample data! Compile and run the project, and you should see something like this:

Table View with Plain Style

However, if you scroll the table view up and down a bit, you’ll notice that the headers “float” as you scroll through the sections:

Table View with Plain Style - Floating Headers

This is the standard behavior for table views set with the “plain” style. However, with the style we’re going for, we don’t want the headers to float like that – we want them to stay with the rows as a single unit. And luckily, that is the behavior of table views set with the “grouped” style!

So let’s switch that. Open up RootViewController.xib, click on the Table View in the xib, and set the style to “Grouped”:

Table View Style Setting

Great! Save RootViewController.xib and rerun the project, and now we should have a populated (but semi-ugly) table view:

Table View with Grouped Style

So let’s pretty this up with Core Graphics! But before we begin, let’s discuss the look we’re going for.

Table View Style Analyzed

To get our end result, we’re going to draw the table view in three different sections: the table header, cells, and footer:

Table View Analyzed

In this article, we’re going to start by drawing the cells, so let’s take a closer look at what they look like:

Table View Cells Zoomed

Note a couple things here:

  • The cells are a gradient from white to light gray.
  • Each cell has a white border around the edges to provide definition (except for the last cell, which just has it on the sides).
  • Each cell has a single gray line to separate between it and the next cell (except for the last cell).
  • The paper is indented a bit from the actual edges of the cell, to line up with the “dropped down paper” from the header.

By the way – what this simulates is light shining down on an angle to the top of an iPhone (i.e where a light would usually be in a room). So for anything that’s raised, the top should be highlighted (white) and the bottoms should have a shadow (gray). You’ll see that in a lot of UI designs, and in future articles in this series!

So in theory to do draw the cells, we just need to know how to draw a gradient and some lines with Core Graphics. Should be pretty easy, eh? So let’s get started!

Hello, Core Graphics!

Whenever you want to do custom drawing on iOS, your drawing code should be within a UIView. There’s a special method called drawRect that you put all your drawing code inside.

To get started, let’s just create a “Hello, World” view that paints the entire view red, then set that as a background of our table view cell to make sure it works.

So make sure your “Classes” group is selected under “Groups & Files”, go to “File\New File…”, choose iOS\Cocoa Touch Class, Objective-C class, make sure “Subclass of UIView” is selected, and click Next. Name the file “CustomCellBackground.m”, make sure “Also create CustomCellBackground.h” is checked, and click “Finish”.

We don’t need to make any changes to the header file, so switch directly over to CustomCellBackground.m and make the following changes:

// Uncomment drawRect and replace the contents with the following:
CGContextRef context = UIGraphicsGetCurrentContext();
 
CGColorRef redColor = 
    [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0].CGColor;
 
CGContextSetFillColorWithColor(context, redColor);
CGContextFillRect(context, self.bounds);

Ok, there’s a bunch of new stuff here, so let’s explain this bit by bit.

In the first line, we call a method called UIGraphicsGetCurrentContext() to get the Core Graphics Context that we’ll use in the rest of the method. I like to think of the context as the “canvas” we’ll be painting within. In this case our “canvas” is the view, but you can get other types of contexts as well, such as an offscreen buffer that you can later turn into an image.

One of the interesting things about contexts is that they are stateful. That means that when you call methods to change something like the fill color for example, the fill color will remain set to that color until you change it to something different later.

In fact, that’s exactly what we do on the third line – we use CGContextSetFillColorWithColor to set the fill color to red, any time we fill a shape in the future.

You might have noticed that when we called that method, we couldn’t provide a straight UIColor – we had to provide a CGColorRef instead. Luckily, it’s very easy to convert the easy-to-use UIColor to a CGColor – just by accessing the CGColor property of the UIColor.

Finally, in the last line, we call a method to fill a given rectangle (using whatever fill color has been set in the context previously). For the rectangle, we pass the bounds of the view.

Now that we have a pretty red view, let’s set it as the background of our table view cell! Just make the following modification to RootViewController.m:

// At top of file
#import "CustomCellBackground.h"
 
// Inside RootViewController.m, in the tableView:cellForRowAtIndexPath method, 
//   inside the cell == nil case, after the call to initWithStyle:
cell.backgroundView = [[[CustomCellBackground alloc] init] autorelease];
cell.selectedBackgroundView = [[[CustomCellBackground alloc] init] autorelease];
 
// At end of function, right before return cell:
cell.textLabel.backgroundColor = [UIColor clearColor];

What we did here was set the backgroundView and selectedBackgroudnView of each cell as we make it to our new CustomCellBackground class. We also change the background color of the text label to clear, so our background can show through.

Compile and run the app, and you should see the following:

Hello, Core Graphics!

Awesome, we can draw with Core Graphics! And believe it or not, we already learned a bunch of really important techniques – how to get a context to draw in, how to change the fill color, and how to fill rectangles with a color. You can make some pretty nice UI with just that!

But we’re going to take it a step further, and learn about one of the most useful techniques to make excellent UIs: gradients!

Drawing Gradients

We’re going to be drawing a lot of gradients in this project, so let’s add our gradient drawing code into a helper function. That way we won’t be repeating ourselves throughout the project!

So make sure your “Classes” group is selected under “Groups & Files”, go to “File\New File…”, choose iOS\Cocoa Touch Class, Objective-C class, make sure “Subclass of NSObject” is selected, and click Next. Name the file “Common.m”, make sure “Also create Common.h” is checked, and click “Finish”.

Now delete everything in Common.h and replace it with the following:

#import <Foundation/Foundation.h>
 
void drawLinearGradient(CGContextRef context, CGRect rect, CGColorRef startColor, 
    CGColorRef  endColor);

We’re not actually making a class here since we don’t need any state – we’re just defining a global function we’re about to write.

Now switch over to Common.m, delete everything inside, and replace it with the following:

#import "Common.h"
 
void drawLinearGradient(CGContextRef context, CGRect rect, CGColorRef startColor, 
    CGColorRef  endColor) {
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat locations[] = { 0.0, 1.0 };
 
    NSArray *colors = [NSArray arrayWithObjects:(id)startColor, (id)endColor, nil];
 
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, 
        (CFArrayRef) colors, locations);
 
    // More coming... 
}

There’s a lot in this function, so we’re going to explain it in two parts. We’ll start with the part we just wrote, that creates the gradient that we will draw later.

The first thing we need is to get a color space with which we’ll draw the gradient. There’s a lot you can do with color spaces, but 99% of the time you just want a standard device-dependent RGB color space, so we simply use the function CGColorSpaceCreateDeviceRGB to get the reference we need.

Next, we set up an array that tracks the location of each color within the range of the gradient. A value of 0 would mean the start of the gradient, 1 would mean the end of the gradient. We just have two colors, and we want the first to be at the start and the second to be at the end, so we pass in 0 and 1.

Note that you could have three or more colors in a gradient if you want, and you could set where each color would begin in the gradient here. This can be useful for certain effects.

After that, we create an array with the colors that were passed into our function. We use a plain old NSArray here for convenience sake.

Then we create our gradient with CGGradientCreateWithColors, passing in the color space, color array, and locations we previously made. Note we have to convert the NSArray to a CFArrayRef – this is easy, we can just do this by casting.

The reason this works is because NSArray is “toll-free bridged” to a CGArrayRef – basically a fancy way of saying that Apple wrote all the magic code to make the conversion as easy as casting.

We now have a gradient reference, but it hasn’t actually drawn anything yet – it’s just a pointer to the information we can use when actually drawing it later. So let’s do that now! Add the following to drawLinearGradient after the “More coming” comment:

CGPoint startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
CGPoint endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
 
CGContextSaveGState(context);
CGContextAddRect(context, rect);
CGContextClip(context);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGContextRestoreGState(context);
 
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);

The first thing we do is calculate the start and end point for where we want to draw the gradient. We just set this as a line from the “top middle” to the “bottom middle” of the rectangle. Note that we use some helper functions from CGGeometry.h (such as CGRectGetMidX) to calculate these (and make our code read cleaner!)

The rest of the code helps us draw a gradient into the provided rectangle – the key function being CGContextDrawLinearGradient. The weird thing about that function, though, is that it fills up the entire drawing region with the gradient. I.e. there’s no way to set it to only fill up a sub area with the gradient.

Well… without clipping, that is! Clipping is an awesome feature in Core Graphics where you can restrict drawing to an arbitrary shape. All you have to do is add the shape to the context, but then instead of filling it like you usually would, you call CGContextClip. And all future drawing will be restricted to that region!

So that’s what we do here. We add our rectangle to the context, clip to that, then call CGContextDrawLinearGradient, passing in all of the variables we’ve set up before.

So what’s this stuff about CGContextSaveCGState/CGContextRestoreCGState all about? Well, remember that Core Graphics is a state machine, and once you’ve set something it stays that way until you change it back.

Well, we’ve just clipped to a region, so unless we do something about it we’ll never be able to draw outside of that region again!

Well that’s where CGContextSaveCGState/CGContextRestoreCGState come to the rescue. With these we can save the current setup of our context to a stack, and then pop it back later when we’re done and get back to where we were.

There’s just one last thing – we need to call CGGradientRelease to free up the memory created by CGGradientCreateWithColors earlier (and CGColorSpaceRelease too, thanks @Jim!).

That’s it! So let’s give this function a shot inside our cell background. Open up CustomCellBackground.m and make the following changes:

// Add to top of file
#import "Common.h"
 
// Replace contents of drawRect with the following:
CGContextRef context = UIGraphicsGetCurrentContext();
 
CGColorRef whiteColor = [UIColor colorWithRed:1.0 green:1.0 
    blue:1.0 alpha:1.0].CGColor; 
CGColorRef lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 
    blue:230.0/255.0 alpha:1.0].CGColor;
 
CGRect paperRect = self.bounds;
 
drawLinearGradient(context, paperRect, whiteColor, lightGrayColor);

Compile and run your project, and you should see the following:

Cells with Gradients

Wow, it’s amazing what a simple gradient can do!

Stroking Paths

It’s looking good so far, but we’re going to add some subtle touches to make it “pop” just a little bit more. We’ll draw a white rectangle around the edges, and a gray separator between cells.

We already know how to fill rectangles – well it turns out drawing lines around the rectangles (i.e. stroking rectangles) is just as easy!

Let’s give it a shot. Make the following changes to CustomCellBackground.m:

// Add a color for red up where the colors are
CGColorRef redColor = [UIColor colorWithRed:1.0 green:0.0 
    blue:0.0 alpha:1.0].CGColor;
 
// Add down at the bottom
CGRect strokeRect = CGRectInset(paperRect, 5.0, 5.0);
 
CGContextSetStrokeColorWithColor(context, redColor);
CGContextSetLineWidth(context, 1.0);
CGContextStrokeRect(context, strokeRect);

We’re going to draw this rectangle with a red stroke and make it in the middle of the cell, in order to make it easy to see at first. So we create a color, and shrink the rectangle a bit with the CGRectInset helper function.

If you haven’t seen it before, all the CGRectInset method does is subtract a given amount from the X and Y sides of a rectangle, and return to you the results.

We then set the stroke color to red, set the line width to 1 point wide, and call CGContextStrokeRect to stroke the rectangle.

Compile and run the project and you’ll see the following:

Fuzzy 1 Pixel Lines in Core Graphics

It looks OK at first… but then again, maybe a little fuzzy or weird eh? Well if you zoom in you’ll see some odd behavior:

Fuzzy 1 Pixel Lines in Core Graphics - Zoomed

We told it to draw with 1 point (which should equate to 1 pixel for the iPhone 3GS), but it appears to actually be drawing over multiple pixels… what’s going on?

1 Point Lines and Pixel Boundaries

Well, it turns out that when Core Graphics strokes a path, it draws the stroke on the middle of the exact edge of the path.

In our case, the edge of the path is the rectangle we wish to fill. So when drawing a 1 pixel line along that edge, half of the line (1/2 pixel) will be on the inside of the rectangle, and the other half of the line (1/2 pixel) will be on the outside of the rectangle.

But of course, since there’s no way to draw 1/2 a pixel, instead Core Graphics uses anti-aliasing to draw in both pixels, but just a lighter shade to give the appearance that it is only a single pixel drawn.

But don’t want no anti-aliasing, we want just one pixel, damnit! There are several ways to fix this:

  • You can use clipping to cut out the undesirable pixels
  • You can disable antialiasing and also modify the rectangle boundaries to make sure the stroke is where you want
  • You can modify the path to stroke so it takes the 1/2 pixel effect into consideration

In this tutorial, we’re going to go with option #3 and modify the rectangle to take the stroke behavior into consideration. Let’s create a helper method to modify a rectangle for a 1 pixel stroke.

So open up Common.h and add the following declaration at the bottom of the file:

CGRect rectFor1PxStroke(CGRect rect);

Then add the following modification to Common.m:

CGRect rectFor1PxStroke(CGRect rect) {
    return CGRectMake(rect.origin.x + 0.5, rect.origin.y + 0.5, 
        rect.size.width - 1, rect.size.height - 1);
}

Here we modify the rectangle so the edge is halfway through the inside pixel of the original rectangle, so the stroke behavior works correctly.

So let’s call this back in CustomCellBackground.m:

// Replace strokeRect declaration with the following:
CGRect strokeRect = rectFor1PxStroke(CGRectInset(paperRect, 5.0, 5.0));

Now if you compile and run the rectangle strokes should be nice and sharp:

1 Pixel Lines in Core Graphics - Sharp

Ok nice. Now let’s finish this up to make it the right color and location. Make the following changes to CustomCellBackground.m:

// Replace strokeRect declaration and setting stroke color with the following:
CGRect strokeRect = paperRect;
strokeRect.size.height -= 1;
strokeRect = rectFor1PxStroke(strokeRect);
 
CGContextSetStrokeColorWithColor(context, whiteColor);

Here we reduce the height of the paper rect by 1 to leave room for the separator, convert it, and stroke the color with white.

Compile and run your project, and there should now be a subtle white border around the cells:

Custom Cells with White Border

Next, let’s add a light gray separator between cells!

Drawing Lines

Since we’re going to be drawing several lines in our project, let’s make a helper method for it. Add the following to Common.h:

void draw1PxStroke(CGContextRef context, CGPoint startPoint, CGPoint endPoint, 
    CGColorRef color);

And the following to Common.m:

void draw1PxStroke(CGContextRef context, CGPoint startPoint, CGPoint endPoint, 
    CGColorRef color) {
 
    CGContextSaveGState(context);
    CGContextSetLineCap(context, kCGLineCapSquare);
    CGContextSetStrokeColorWithColor(context, color);
    CGContextSetLineWidth(context, 1.0);
    CGContextMoveToPoint(context, startPoint.x + 0.5, startPoint.y + 0.5);
    CGContextAddLineToPoint(context, endPoint.x + 0.5, endPoint.y + 0.5);
    CGContextStrokePath(context);
    CGContextRestoreGState(context);        
 
}

Ok, let’s go through this. At the beginning and end we save/restore our context so we don’t leave any of the changes we made around.

Then we set the line cap of the line. The default is for a line to have a “butt” ending, which means the line ends EXACTLY at the ending point of the line.

But this isn’t good for us since we’re starting and ending the line 1/2 point in to fix the stroke behavior. So instead, we set the line cap to have a “square” ending, which makes the line extend 1/2 of the line width beyond the end – in our case 1/2 point – perfect!

We then set the color and line width as usual.

We then do the actual drawing of a line. To draw a line in Core Graphics, you first move to point A (nothing is drawn yet), and then add a line to point B (which adds the line from point A to point B into the context). You can then call CGContextStrokePath to stroke the line.

That’s it! So let’s use it to draw a separator line by making the following changes CustomCellBackground.m’s drawRect:

// Add in color section
CGColorRef separatorColor = [UIColor colorWithRed:208.0/255.0 green:208.0/255.0 
    blue:208.0/255.0 alpha:1.0].CGColor;
 
// Add at bottom
CGPoint startPoint = CGPointMake(paperRect.origin.x, 
    paperRect.origin.y + paperRect.size.height - 1);
CGPoint endPoint = CGPointMake(paperRect.origin.x + paperRect.size.width - 1, 
    paperRect.origin.y + paperRect.size.height - 1);
draw1PxStroke(context, startPoint, endPoint, separatorColor);

Compile and run your project, and now there should be a nice separator between the cells!

Custom Cells with Separator

Where To Go From Here?

Here is a sample project with all of the code we’ve developed so far in the above tutorial.

At this point you should be familiar with some pretty cool and powerful techniques with Core Graphics – filling and stroking rectangles, drawing lines and gradients, and clipping to paths! Not to mention our table view is starting to look pretty cool.

But there’s more! We haven’t covered how to add drop shadows yet, or arcs, gloss effects, and other cool stuff – which we cover in the next tutorial as we add a cool looking header to our table view!

In the meantime, if you have any questions, suggestions, or comments, please fire away! :]


Category: iPhone

Tags: , , , , ,

I'd love to hear your thoughts!