If you're new here, you may want to subscribe to my RSS feed or follow me on Twitter. Thanks for visiting!
Welcome back to another session of Core Graphics 101! In this tutorial series, we’re covering how to get started with Core Graphics 101 – with practical examples!
In tutorials one, two, and three, we covered how to customize a table view from start to finish – just with Core Graphics!
In this tutorial, we’re going to tackle a different practical example – how to customize a UIButton.
In the process, we’ll learn how to draw rounded rects, how to easily tint your Core Graphics drawings, and reinforce some of the concepts we’ve already covered.
As Alex Curylo from Under The Bridge has mentioned many times, there are a lot of good options out there for how to customize your UIButtons already. My personal favorite for quick and easy button making is Button Maker by Dermot Daly, by the way.
But I think what’s been missing in this discussion is a detailed tutorial for how to customize the buttons yourself, from start to finish, with Core Graphics. It’s pretty simple, and this way you can get the exact look you’re going for in your app.
So let’s get started and make some buttons!
Getting Started
Create a new project in XCode using the View-based Application template, and name the project “CoolButton”.
Then 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 “CoolButton.m”, make sure “Also create CoolButton.h” is checked, and click “Finish”.
Then replace the contents of CoolButton.h with the following:
#import <UIKit/UIKit.h> @interface CoolButton : UIButton { CGFloat _hue; CGFloat _saturation; CGFloat _brightness; } @property CGFloat hue; @property CGFloat saturation; @property CGFloat brightness; @end |
Notice that we’re not actually subclassing UIView – we’re subclassing UIButton instead. We also define some properties that will allow us to configure the hue, saturation, and brightness for the view.
Next make the following changes to CoolButton.m:
// Under @implementation @synthesize hue = _hue; @synthesize saturation = _saturation; @synthesize brightness = _brightness; // Delete initWithFrame and add the following: -(id) initWithCoder:(NSCoder *)aDecoder { if ((self = [super initWithCoder:aDecoder])) { self.opaque = NO; self.backgroundColor = [UIColor clearColor]; _hue = 1.0; _saturation = 1.0; _brightness = 1.0; } return self; } // Uncomment drawRect and replace the contents with: CGContextRef context = UIGraphicsGetCurrentContext(); CGColorRef color = [UIColor colorWithHue:_hue saturation:_saturation brightness:_brightness alpha:1.0].CGColor; CGContextSetFillColorWithColor(context, color); CGContextFillRect(context, self.bounds); // Add to the bottom of the file - (void)setHue:(CGFloat)hue { _hue = hue; [self setNeedsDisplay]; } - (void)setSaturation:(CGFloat)saturation { _saturation = saturation; [self setNeedsDisplay]; } - (void)setBrightness:(CGFloat)brightness { _brightness = brightness; [self setNeedsDisplay]; } |
Here we just initializing the variables and filling the whole thing with our configured color to make sure everything’s working OK to start.
Note that we’re using a different constructor to make the color this time – rather than colorWithRed:Green:Blue, we use colorWithHue:saturation:brightness. This will make things a bit easier for us later on.
The last thing we do here is override the setters for the hue, saturation, and brightness properties so we call setNeedsDisplay when they are called. This will force our view to redraw when the user changes the color of the button.
Now switch over to CoolButtonViewController.h and make the following changes:
// Before @interface @class CoolButton; // Inside @interface CoolButton *_button; // After @interface @property (retain) IBOutlet CoolButton *button; - (IBAction)hueValueChanged:(id)sender; - (IBAction)saturationValueChanged:(id)sender; - (IBAction)brightnessValueChanged:(id)sender; |
Here we’re declaring a reference to the button (which we’ll make in Interface Builder) and some callbacks for when the configuratoin slider values change (which we’ll also add in Interface Builder).
So let’s go ahead and do that now. Open up CoolButtonViewController.m, and drag a UIButton, 3 UILabels, and 3 UISliders onto the view so it looks something like this:
Next, change the class of the UIButton to be a CoolButton by going to the Identity Inspector and changing the Class dropdown to CoolButton:
Also, make sure the Attributes Inspector is selected, and switch the drawing type of the Button to Custom to remove the default rounded corners of the button.
Then, control-drag from “File’s Owner” to the CoolButton and connect it to the button outlet. Similarly, control-drag from each slider up to “File’s Owner” to connect it to the appropriate value changed callback.
One last thing before we try it out. Make the following changes to CustomViewController.m:
// In the import section #import "CoolButton.h" // Under @implementation @synthesize button = _button; // In viewDidUnload self.button = nil; // In dealloc [_button release]; _button = nil; // Add to the bottom of the file - (IBAction)hueValueChanged:(id)sender { UISlider *slider = (UISlider *)sender; _button.hue = slider.value; } - (IBAction)saturationValueChanged:(id)sender { UISlider *slider = (UISlider *)sender; _button.saturation = slider.value; } - (IBAction)brightnessValueChanged:(id)sender { UISlider *slider = (UISlider *)sender; _button.brightness = slider.value; } |
Note that by default UISliders are set to go from 0.0 to 1.0 – which is perfect for our hue, saturation, and brightness values, which also range from 0.0 to 1.0, so we can just set them directly.
Build and run the project, and if all works well you should be able to play around with the sliders to fill the “CoolButton” with various colors:
Drawing Rounded Rectangles
It’s true that you can have square buttons, but these days it’s fashionable to have rounded rectangle buttons for the most part instead.
In the last tutorial, we covered how to draw arcs using the CGContextAddArc API. So we could definitely use that to draw an arc at each corner, and draw lines to connect them, based on stuff we already know.
But there’s an even easier way, where we don’t have to do quite so much math, and that actually fits in really well with drawing rounded rectangles. It’s the CGContextAddArcToPoint API.
The CGContextAddArcToPoint API lets you specify the arc to draw by specifying two tangent lines and a radius. The following diagram from the Quartz2D Programming Guide shows it pretty well:
So for the case of a rectangle, we obviously know the tangent lines for each arc we want to draw – they are just the edges of the rectangle! And we can specify the radius based on how rounded we want the rectangle to be – the larger, the more rounded.
The other neat thing about this function is if the current point in the path isn’t set to where you tell the arc to begin drawing, it will draw a line from the current point to the beginning of the path. So you can use this as a shortcut to draw a rounded rectangle in just a couple calls.
Since we’re going to create a bunch of rounded rects in this tutorial and in the future, let’s add a helper function to Common.h/m to create a path for a rounded rect, given a rectangle.
If you don’t have it already, download Common.h/m where we left it off last time, and add it to your project.
Then add the following to Common.h:
CGMutablePathRef createRoundedRectForRect(CGRect rect, CGFloat radius); |
And the following to the bottom of Common.m:
CGMutablePathRef createRoundedRectForRect(CGRect rect, CGFloat radius) { CGMutablePathRef path = CGPathCreateMutable(); CGPathMoveToPoint(path, NULL, CGRectGetMidX(rect), CGRectGetMinY(rect)); CGPathAddArcToPoint(path, NULL, CGRectGetMaxX(rect), CGRectGetMinY(rect), CGRectGetMaxX(rect), CGRectGetMaxY(rect), radius); CGPathAddArcToPoint(path, NULL, CGRectGetMaxX(rect), CGRectGetMaxY(rect), CGRectGetMinX(rect), CGRectGetMaxY(rect), radius); CGPathAddArcToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMaxY(rect), CGRectGetMinX(rect), CGRectGetMinY(rect), radius); CGPathAddArcToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMinY(rect), CGRectGetMaxX(rect), CGRectGetMinY(rect), radius); CGPathCloseSubpath(path); return path; } |
The above code is drawing the rounded rect in the following order:
- Move to the center of the top line segment.
- Add an arc for the upper right corner. Before drawing an arc, CGPathAddArcToPoint will draw a line from the current position (middle of the rect) to the beginning of the arc for us.
- Similarly, add an arc for the lower right corner and the connecting line.
- Similarly, add an arc for the lower left corner and the connecting line.
- Similarly, add an arc for the upper left corner and the connecting line.
- Then connect the ending point of the arc with the starting point with CGPathCloseSubpath.
Also note in the above that we use some more of the useful helper methods from CGGeometry.h to pull out various locations in the rectangle we’re provided.
Ok let’s see how this works! Open up CoolButton.m and make the following modifications:
// At top of file #import "Common.h" // Replace the contents of drawRect with the following: CGContextRef context = UIGraphicsGetCurrentContext(); CGColorRef outerTop = [UIColor colorWithHue:_hue saturation:_saturation brightness:_brightness alpha:1.0].CGColor; CGColorRef shadowColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.5].CGColor; CGFloat outerMargin = 5.0f; CGRect outerRect = CGRectInset(self.bounds, outerMargin, outerMargin); CGMutablePathRef outerPath = createRoundedRectForRect(outerRect, 6.0); if (self.state != UIControlStateHighlighted) { CGContextSaveGState(context); CGContextSetFillColorWithColor(context, outerTop); CGContextSetShadowWithColor(context, CGSizeMake(0, 2), 3.0, shadowColor); CGContextAddPath(context, outerPath); CGContextFillPath(context); CGContextRestoreGState(context); } |
Ok, so we define our two colors, then we use CGRectInset to get a slightly smaller rectangle (5 pixels on each side) where we’ll draw the rounded rect. We make it smaller so we have space to draw a shadow on the outside.
Next, we call the function we just wrote to create a path for our rounded rect. We then set the fill color and shadow, add the path to our context, and call FillPath to fill it with our current color.
Note we only want to run the code if our button isn’t currently highlighted (i.e. being tapped upon).
Compile and run the app, and if all works well you should see the following:
Styling our Button
Ok, we have something that looks somewhat like a button now! But it doesn’t look very good yet :[
So let’s do something about that! Let’s add several incremental improvements to our button.
Open up CoolButton.m and make the following mods:
// Replace the colors section with the following CGColorRef blackColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:1.0].CGColor; CGColorRef highlightStart = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.4].CGColor; CGColorRef highlightStop = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.1].CGColor; CGColorRef shadowColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.5].CGColor; CGColorRef outerTop = [UIColor colorWithHue:_hue saturation:_saturation brightness:1.0*_brightness alpha:1.0].CGColor; CGColorRef outerBottom = [UIColor colorWithHue:_hue saturation:_saturation brightness:0.80*_brightness alpha:1.0].CGColor; CGColorRef innerStroke = [UIColor colorWithHue:_hue saturation:_saturation brightness:0.80*_brightness alpha:1.0].CGColor; CGColorRef innerTop = [UIColor colorWithHue:_hue saturation:_saturation brightness:0.90*_brightness alpha:1.0].CGColor; CGColorRef innerBottom = [UIColor colorWithHue:_hue saturation:_saturation brightness:0.70*_brightness alpha:1.0].CGColor; // Add the following to the bottom CGContextSaveGState(context); CGContextAddPath(context, outerPath); CGContextClip(context); drawGlossAndGradient(context, outerRect, outerTop, outerBottom); CGContextRestoreGState(context); |
First thing we do is set up a bunch of colors that we’ll use later. There are a few generic colors, and then several colors based on the passed in parameters. Our base color is exactly how we’re set up, then we have several colors set up to be darker than the current color, to varying degress.
If you define your colors in this way, it makes it really easy to change the colors of your views, which can be quite handy for reusing code!
The next thing we do is clip to our rounded rect, and fill it with a gradient (rather than a single color). Compile and run and our button is starting to look better:
Now let’s add an inner path that has a slightly different gradient than the outer path, to create a bevel-type effect. Make the following mods to CoolButton.m:
// Add after the creation of the outerPath CGFloat innerMargin = 3.0f; CGRect innerRect = CGRectInset(outerRect, innerMargin, innerMargin); CGMutablePathRef innerPath = createRoundedRectForRect(innerRect, 6.0); // At the bottom CGContextSaveGState(context); CGContextAddPath(context, innerPath); CGContextClip(context); drawGlossAndGradient(context, innerRect, innerTop, innerBottom); CGContextRestoreGState(context); |
Here we shrink the rectangle again with CGRectInset, then get a rounded rect for that and run a gradient over it. Compile and run and there’s a subtle improvement:
Now let’s add a very subtle highlight on the top, if the button is selected. Make some more mods to CoolButton.m:
// After the creation of the innerPath CGFloat highlightMargin = 2.0f; CGRect highlightRect = CGRectInset(outerRect, highlightMargin, highlightMargin); CGMutablePathRef highlightPath = createRoundedRectForRect(highlightRect, 6.0); // At the bottom if (self.state != UIControlStateHighlighted) { CGContextSaveGState(context); CGContextSetLineWidth(context, 4.0); CGContextAddPath(context, outerPath); CGContextAddPath(context, highlightPath); CGContextEOClip(context); drawLinearGradient(context, outerRect, highlightStart, highlightStop); CGContextRestoreGState(context); } |
We’re basically creating another rounded rect slightly smaller than the outerRect, and filling the area between the two rectangles with an alpha highlight gradient, using our Even-Odd Clip technique from last tutorial.
Compile and run it to check it out – it’s a very subtle touch so you might not notice it:
Ok let’s wrap this up. Add the following code to the bottom of drawRect:
CGContextSaveGState(context); CGContextSetLineWidth(context, 2.0); CGContextSetStrokeColorWithColor(context, blackColor); CGContextAddPath(context, outerPath); CGContextStrokePath(context); CGContextRestoreGState(context); CGContextSaveGState(context); CGContextSetLineWidth(context, 2.0); CGContextSetStrokeColorWithColor(context, innerStroke); CGContextAddPath(context, innerPath); CGContextClip(context); CGContextAddPath(context, innerPath); CGContextStrokePath(context); CGContextRestoreGState(context); CFRelease(outerPath); CFRelease(innerPath); CFRelease(highlightPath); |
All we do here is stroke the outer path with black (2 points to avoid the 1px issues), and the inner path with a 1 point stroke.
“OMGBGQ”, you may say, “that is a 2 point stroke, not a 1 point stroke!” Well, we’re using a different technique to solve the 1px issue here – the “clipping mask” technique I referred to earlier. Basically we set the stroke to be 2 points, and then clip off the outside region.
At the end, we release the paths we’ve created.
Compile and run the app, and check out our button now!
It’s amazing what a few strokes can do!
And hey – get your mind out of the gutter :P
Highlighting the Button
Our button looks pretty cool, but doesn’t act like a button. There’s no indication when the button is pressed or not.
Luckily, Jeff LaMarche demonstrates how to handle this in one of his posts on the subject.
The basic idea is we need to override the touch events to tell our button to redisplay itself, since it might need an update due to being selected.
So make the following modifications to CoolButton.m:
// Add the following methods to the bottom - (void)hesitateUpdate { [self setNeedsDisplay]; } -(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; [self setNeedsDisplay]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; [self setNeedsDisplay]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesCancelled:touches withEvent:event]; [self setNeedsDisplay]; [self performSelector:@selector(hesitateUpdate) withObject:nil afterDelay:0.1]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; [self setNeedsDisplay]; [self performSelector:@selector(hesitateUpdate) withObject:nil afterDelay:0.1]; } |
Compile and run the project, and you’ll see that there’s a difference as you tap the button now – the highlight and tap disappears. But let’s make it a bit better with one more mod:
// At beginning of function CGFloat actualBrightness = _brightness; if (self.state == UIControlStateHighlighted) { actualBrightness -= 0.10; } // Then replace all further cases in the function that use // "_brightness" to define colors with "actualBrightness". |
Recompile and run and now the button should look pretty good when you tap it!
Where To Go From Here?
Here is a sample project with all of the code we developed in the above tutorial.
Now that we’ve gone through all of the steps to create custom buttons from scratch, you should be intimately familiar with how to customize every aspect of the button to your liking for your project’s style!
Next up is one final tutorial in the Core Graphics 101 series, where we’ll discuss how to use patterns for a cool effect!
Category: iPhone

















