GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Designing for iOS: Taming UIButton

Regardless of the platform you are designing for, getting familiar with its technologies and frameworks is key to refining your design process; even a basic understanding of the technical environment and its constraints can help you streamline the transition from Photoshop comps to design implementation, or completely bypass the former where appropriate.

In this tutorial, we’ll cover four techniques of varying levels of difficulty to draw a custom UIButton, the primary button class in iOS. If you are just getting started, I recommend reading Mike Rundle’s introduction and giving these tutorials a go to get a more hands-on experience.

Setup

Start by creating a single view application in Xcode using Apple’s default settings. In the file navigator, select MainStoryboard.storyboard then drag-and-drop a Round Rect Button from the bottom-left objects library to the view canvas. Center the button in the canvas and resize it to your liking, then make few copies of it as shown in the screenshot below.

Dran-and-drop UIButton

Make sure to choose the label font and color using the Attribute inspector in the right-hand side of the Xcode window.

Painting Pixels

The most common and straightforward approach consists in using background images to customize the stock UI controls provided by Apple.

Full-sized Assets

Difficulty: ★☆☆☆☆

The first option involves using a full-sized graphic as the background of your UIButton instance. To do so, design the button (normal and highlighted states) in your favorite graphic editor then export the assets as uncompressed PNGs in both standard and double resolutions.

Full-sized assets

Import the images to your project by dragging them into the File navigator. In Interface builder, change the button type to custom in the top-right Attributes Inspector.

Change button type

Then add your images as backgrounds for both the default and the highlighted states using the drop-down shown below.

Change background

You can also set background images in code by control-dragging your button to the @interface section of the main view controller as an IBOutlet:

@property (weak, nonatomic) IBOutlet UIButton *fullSizeImage;

… then calling the setBackgroundImage method inside viewDidLoad for each state:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.fullSizeImage setBackgroundImage:[UIImage imageNamed:@"button.png"] forState:UIControlStateNormal];
    [self.fullSizeImage setBackgroundImage:[UIImage imageNamed:@"buttonHighlighted.png"] forState:UIControlStateHighlighted];
}

It is worthing noting at this point that anything drawn in code is not previewable in Interface builder. Run the app on the iOS simulator by hitting cmd+R to see the result.

Advantages

  • Absolute creative freedom.
  • No coding prerequisites if using Interface Builder.

Disadvantages

  • Larger app bundles.
  • Less reusable across the app (size-constrained.)
  • Even minor edits require re-exporting the assets.

Resizable Assets (iOS 5+)

Difficulty: ★★★☆☆

A recently introduced second option consists in using a resizable image as a button background after having set its resizable and non-resizable areas in code. Start by making a pill-shaped background image in your graphic editor.

Resizable background images

Next, you should instruct Xcode what areas should be stretched to fit the dimension of the button. For instance, you can prevent the four corners of the background image from being stretched using cap insets. To see this in action, import the assets into Xcode and create a resizable image object to be used with setBackgroundImage (inside viewDidLoad):

UIImage *resizableButton = [[UIImage imageNamed:@"resizableButton.png" ] resizableImageWithCapInsets:UIEdgeInsetsMake(17, 5, 17, 5)];
UIImage *resizableButtonHighlighted = [[UIImage imageNamed:@"resizableButtonHighlighted.png" ] resizableImageWithCapInsets:UIEdgeInsetsMake(17, 5, 17, 5)];

The UIEdgeInsets argument takes four floats corresponding to the distance (in density-independent pixels) separating the caps from each side of the image in the following order: top, left, bottom, right.

Cap insets

For the sake of clarity, the center area in the example above is much wider than it should actually be. In most cases, a 1pt stretchable area is what you should be aiming for.

Unlike full-sized images, setting a resizable image as a background can only be done in code (same steps as the previous method):

// Drag-and-drop the UIButton instance to the @interface section of your view controller
@property (weak, nonatomic) IBOutlet UIButton *resizableImage;

// Inside viewDidLoad
[self.resizableImage setBackgroundImage:resizableButton forState:UIControlStateNormal];
[self.resizableImage setBackgroundImage:resizableButtonHighlighted forState:UIControlStateHighlighted];

In iOS 6, Apple updated the resizableImageWithCapInsets method to accept a resizingMode argument that explicitly instructs Xcode which of the two available resizing modes to use: tiling or stretching. When unspecified, the former is used.

Advantages

  • Lighter app bundles.
  • Reusable assets across the app.

Disadvantages

  • Edits involve re-exporting the assets and might involve resetting the cap insets as well.

Unleash the CALayer Within

Difficulty: ★★★★☆

Update: The code in this technique has been refactored for better performance with the help of Joris Kluivers.

The CALayer (Core Animation layer) object is the fundamental drawing unit in iOS; a view draws itself into its layer before this latter appears on screen. You’d be surprised at how much you can accomplish without having to draw a single pixel in Photoshop.

First, we need to create a UIButton subclass where our drawing code will end up. Hit cmd+N and create a new Objective-C class by subclassing UIButton.

Subclassing UIButton

In Interface builder, set the class of the target button to CBLayer (or whatever you named your subclass) using the top-most field in the Identity inspector.

Set UIButton class

Go back to the implementation file of your custom button class (.m extension) and implement initWithCoder, a method that will get called on our button instance in order to unarchive it when the view loads. Between the @implementation and the @end directives:

- (id)initWithCoder:(NSCoder *)coder
{
    // Our custom CALayer drawing will go here
}

To better organize our code, we’ll split the different parts of the button (background, border, inner-glow, etc.) into separate properties and drawing methods, then call each from within initWithCoder. To do so, we’ll start by declaring the properties that will be used in this process above the @implementation directive:

@interface CBLayer ()

@property (strong,nonatomic) CAGradientLayer *backgroundLayer, *highlightBackgroundLayer;
@property (strong,nonatomic) CALayer *innerGlow;

@end

We’ll implement four instance methods (a message that can be sent to a single button instance) to take care of drawing the button and setting its properties. Whenever using CALayer, remember to link the QuartzCore framework in the Build phases tab of the project manager:

Add Quartzcore framework

You’ll also need to import its header file in your subclass using this directive #import.

Let’s start by drawing the button itself:

- (void)drawButton
{
    // Get the root layer (any UIView subclass comes with one)
    CALayer *layer = self.layer;

    layer.cornerRadius = 4.5f;
    layer.borderWidth = 1;
    layer.borderColor = [UIColor colorWithRed:0.77f green:0.43f blue:0.00f alpha:1.00f].CGColor;
}

For the gradients, we’ll use the CAGradientLayer subclass made specifically for this purpose:

- (void)drawBackgroundLayer
{
    // Check if the property has been set already
    if (!_backgroundLayer)
    {
         // Instantiate the gradient layer
        _backgroundLayer = [CAGradientLayer layer];

        // Set the colors
        _backgroundLayer.colors = (@[
                                   (id)[UIColor colorWithRed:0.94f green:0.82f blue:0.52f alpha:1.00f].CGColor,
                                   (id)[UIColor colorWithRed:0.91f green:0.55f blue:0.00f alpha:1.00f].CGColor
                                   ]);

        // Set the stops
        _backgroundLayer.locations = (@[
                                      @0.0f,
                                      @1.0f
                                      ]);

        // Add the gradient to the layer hierarchy
        [self.layer insertSublayer:_backgroundLayer atIndex:0];
    }
}

The highlighted state gradient can be set using a separate - (void)drawHighlightBackgroundLayer method that is identical to the one above, save for he color values. To DRY up your code, it is recommended that you move the layer drawing code to a separate -(void)drawBackgroundLayerWithGradient:(NSArray)colors method.

Next, we will implement an instance method to set the _innerGlow property:

- (void)drawInnerGlow
{
    if (!_innerGlow)
    {
        // Instantiate the innerGlow layer
        _innerGlow = [CALayer layer];

        _innerGlow.cornerRadius= 4.5f;
        _innerGlow.borderWidth = 1;
        _innerGlow.borderColor = [[UIColor whiteColor] CGColor];
        _innerGlow.opacity = 0.5;

        [self.layer insertSublayer:_innerGlow atIndex:2];
    }
}

If we build and run the app at this stage, only the label will be visible on the button. To get our layers onto the screen, we need to call the layer methods from within initWithCoder.

- (id)initWithCoder:(NSCoder *)coder
{
    // Call the parent implementation of initWithCoder
  self = [super initWithCoder:coder];

    // Custom drawing methods
  if (self)
    {
    [self drawButton];
        [self drawInnerGlow];
        [self drawBackgroundLayer];
        [self drawHighlightBackgroundLayer];
  }

  return self;
}

That doesn’t seem to be enough though; we also need to set the frame rectangles of our different layers and update them every time the button is tapped by implementing the layoutSubviews method:

- (void)layoutSubviews
{
  // Set inner glow frame (1pt inset)
  _innerGlow.frame = CGRectInset(self.bounds, 1, 1);

  // Set gradient frame (fill the whole button))
  _backgroundLayer.frame = self.bounds;

  // Set inverted gradient frame
  _highlightBackgroundLayer.frame = self.bounds;

  [super layoutSubviews];
}

Hit cmd+R to see our layers in action. We’ve got one little problem though: the button looks constantly pressed (highlighted) and does not visually react to user taps. Having a closer look at the code, the former appears to be a direct result of calling drawHighlightBackgroundLayer which draws the inverted gradient with a higher z-index (see insertSublayer:layer atIndex:index). To fix this, we need to initially hide the alternate background and make it only visible when the button is tapped. Inside initWithCoder, we’ll add the following line below our drawing methods:

_highlightBackgroundLayer.hidden = YES;

Then we’ll implement setHighlighted, a method that gets called whenever a UIButton is tapped:

- (void)setHighlighted:(BOOL)highlighted
{
    // Hide/show inverted gradient
  _highlightBackgroundLayer.hidden = !highlighted;

  [super setHighlighted:highlighted];
}

When using CALayer, properties are animated by default (cross-fade in this case). We can disable the implicit animation inside setHighlighted:

- (void)setHighlighted:(BOOL)highlighted
{
  // Disable implicit animations
  [CATransaction begin];
  [CATransaction setDisableActions:YES];

    // Hide/show inverted gradient
  _highlightBackgroundLayer.hidden = !highlighted;

  [CATransaction commit];

  [super setHighlighted:highlighted];
}

With this we are good to go. One last thing that you may want to do is override the UIButton method buttonWithType in order to prevent inconsistent results if the button is instantiated in code.

+ (CBLayer *)buttonWithType:(UIButtonType)type
{
    return [super buttonWithType:UIButtonTypeCustom];
}

Advantages

  • No graphics.
  • Reusable and expandable code.
  • Easily editable.
  • Transitions are animatable.

Disadvantages

  • Difficulty to obtain certain effects such as inner shadows and glows.
  • Occasional gradient banding (often on darker gradients).
  • Resource hungry.

Bezier Paths and Beyond

Difficulty: ★★★★★

In this last method, we will be using drawRect (base custom drawing method in iOS) and Core Graphics to design our custom button in code.

Go ahead a create a new UIButton subclass then assign it to a button instance in Interface builder. All the code below should go inside drawRect.

We’ll start by defining the color space and the graphics context, a blank canvas where our custom drawing will take place.

// General Declarations
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = UIGraphicsGetCurrentContext();

Next, we will define the colors and the gradients that will be used throughout our implementation:

// Color Declarations
UIColor *borderColor = [UIColor colorWithRed:0.77f green:0.43f blue:0.00f alpha:1.00f];
UIColor *topColor = [UIColor colorWithRed:0.94f green:0.82f blue:0.52f alpha:1.00f];
UIColor *bottomColor = [UIColor colorWithRed:0.91f green:0.55f blue:0.00f alpha:1.00f];
UIColor *innerGlow = [UIColor colorWithWhite:1.0 alpha:0.5];

// Gradient Declarations
NSArray *gradientColors = (@[
                           (id)topColor.CGColor,
                           (id)bottomColor.CGColor
                           ]);

CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)(gradientColors), NULL);

NSArray *highlightedGradientColors = (@[
                                      (id)bottomColor.CGColor,
                                      (id)topColor.CGColor
                                      ]);

CGGradientRef highlightedGradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)(highlightedGradientColors), NULL);

For the rounded rectangle shapes, we will be using UIBezierPath’s bezierPathWithRoundedRect class method.

// Draw rounded rectangle bezier path
UIBezierPath *roundedRectanglePath = [UIBezierPath bezierPathWithRoundedRect: CGRectMake(0, 0, 280, 37) cornerRadius: 4];
// Use the bezier as a clipping path
[roundedRectanglePath addClip];

// Use one of the two gradients depending on the state of the button
CGGradientRef background = self.highlighted? highlightedGradient : gradient;

// Draw gradient within the path
CGContextDrawLinearGradient(context, background, CGPointMake(140, 0), CGPointMake(140, 37), 0);

// Draw border
[borderColor setStroke];
roundedRectanglePath.lineWidth = 2;
[roundedRectanglePath stroke];

// Draw Inner Glow
UIBezierPath *innerGlowRect = [UIBezierPath bezierPathWithRoundedRect: CGRectMake(1.5, 1.5, 277, 34) cornerRadius: 2.5];
[innerGlow setStroke];
innerGlowRect.lineWidth = 1;
[innerGlowRect stroke];

// Cleanup
CGGradientRelease(gradient);
CGGradientRelease(highlightedGradient);
CGColorSpaceRelease(colorSpace);

In order to get our button to change appearance when highlighted, we’ll have to force drawRect to be called whenever the button is tapped. This can be done through calling setNeedsDisplay inside setHighlighted:

- (void)setHighlighted:(BOOL)highlighted
{
  [self setNeedsDisplay];
  [super setHighlighted:highlighted];
}

If you are not inclined to learn Core graphics and are willing to fork $99, you can use PaintCode, a “graphic editor” that generates the drawing code to go inside drawRect.

Advantages

  • Same as CALayer.
  • More creative freedom.

Disadvantages

  • Gradient banding.
  • Can get unwieldy pretty quickly.

Demo app

Get the demo app here.

Update: Andy Matuschak, a member of the UIKit team, was kind enough to point out in this article’s HN thread that these techniques have different performance ramifications. This is will be most certainly the topic of a future article.

Conclusion

Apple provides different ways to customize the look and feel of your iOS apps, and it’s only getting better with each major iOS release. Learning Objective-C and Core graphics is sure daunting for many designers, but it may be worthwhile considering how it can empower you and help you become more efficient on the long run.

What’s next?

If you found this useful, you might also enjoy: