Swift Tool Belt, Part 8: Extending UIButton with Background Color for State

tool-belt

The eighth item in my Swift Tool Belt is an extension for UIButton. This extension adds a bit of functionality that is sorely missing from UIButton, giving you the ability to set the background color for different button states.

A UIButton can be in many states: default, selected, highlighted, or disabled. There are also many items on a UIButton that you can customize for the different states. For instance, you can change the title, font, text color, shadow color, image, and background image, but not the background color. For some reason, Apple decided not to include it.

iOS 7 notoriously broke away from skeuomorphism and introduced buttons that had no background color. They conveyed their different states and purposes with just text color.
iOS 7 style button with just text

I think the trend is somewhere in the middle of using buttons with just text and a skeuomorphic design. Many of the apps that I have created lately use flat-button design with a solid color background.
flat button
Because of this, not having the ability to change the background color for different states causes difficulties.

Use Background Image

The solution to this problem is to use the available background image property that allows you to set a different image for different states. I found a great thread on Stack Overflow suggested by user Tim. He showed how to dynamically create an image based on the color you want for your background.


import UIKit

extension UIButton {
    // https://stackoverflow.com/questions/14523348/how-to-change-the-background-color-of-a-uibutton-while-its-highlighted
    private func image(withColor color: UIColor) -> UIImage? {
        let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
        UIGraphicsBeginImageContext(rect.size)
        let context = UIGraphicsGetCurrentContext()

        context?.setFillColor(color.cgColor)
        context?.fill(rect)

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return image
    }

    func setBackgroundColor(_ color: UIColor, for state: UIControlState) {
        self.setBackgroundImage(image(withColor: color), for: state)
    }
}

This extension adds two functions: one to create an image with the color you specify, and another to add the missing function setBackgroundColor for state. The size of the image does not matter because once you set it to the background, it will stretch to fill the entire button. If you are using a button with a corner radius, then you may have to set clipsToBounds to true in order to see your rounded edges.

Adding the Colors to Interface Builder

The extension works pretty well if you set the color programmatically by calling setBackgroundColor for state. If you know me, it’s no surprise that I wanted to find a way to expose this idea to Xcode’s Interface Builder. We already have a background color for UIButton that can be used for the default state, so that means we need to add a color to Interface Builder for the disabled, selected, and highlighted state.

Unfortunately, we can’t add our color to the grouping of properties that change for each of the button states. We will have to be content adding separate colors for each of the different states.

colors in interface builder

To get this to work, I added separate properties for each of the background color states and made them @IBInspectable, which exposes them to Interface Builder. The easy part is the setter of these properties since we already have a function in our extension that can set the background color for the state we need.

The difficult part is the getter of the property. The color is not stored anywhere on the UIButton object. Our extension can’t add a property because that is not allowed on extensions. I could create a custom UIButton class and add the properties, but then we have to remember to change the class type on every button. Finally, we could parse the background image to discover what color it is, but that seems like too much work.

Instead, I will take advantage of an Objective C construct where you can add a property to an existing class called associated objects. This will allow me to store the color and retrieve it later when the getter is called.

Here is the full extension that adds the disabled color, highlighted color, and selected color to Interface Builder.


import UIKit
import ObjectiveC

// Declare a global var to produce a unique address as the assoc object handle
var disabledColorHandle: UInt8 = 0
var highlightedColorHandle: UInt8 = 0
var selectedColorHandle: UInt8 = 0

extension UIButton {
    // https://stackoverflow.com/questions/14523348/how-to-change-the-background-color-of-a-uibutton-while-its-highlighted
    private func image(withColor color: UIColor) -> UIImage? {
        let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
        UIGraphicsBeginImageContext(rect.size)
        let context = UIGraphicsGetCurrentContext()

        context?.setFillColor(color.cgColor)
        context?.fill(rect)

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return image
    }

    func setBackgroundColor(_ color: UIColor, for state: UIControlState) {
        self.setBackgroundImage(image(withColor: color), for: state)
    }

    @IBInspectable
    var disabledColor: UIColor? {
        get {
            if let color = objc_getAssociatedObject(self, &disabledColorHandle) as? UIColor {
                return color
            }
            return nil
        }
        set {
            if let color = newValue {
                self.setBackgroundColor(color, for: .disabled)
                objc_setAssociatedObject(self, &disabledColorHandle, color, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            } else {
                self.setBackgroundImage(nil, for: .disabled)
                objc_setAssociatedObject(self, &disabledColorHandle, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }

    @IBInspectable
    var highlightedColor: UIColor? {
        get {
            if let color = objc_getAssociatedObject(self, &highlightedColorHandle) as? UIColor {
                return color
            }
            return nil
        }
        set {
            if let color = newValue {
                self.setBackgroundColor(color, for: .highlighted)
                objc_setAssociatedObject(self, &highlightedColorHandle, color, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            } else {
                self.setBackgroundImage(nil, for: .highlighted)
                objc_setAssociatedObject(self, &highlightedColorHandle, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }

    @IBInspectable
    var selectedColor: UIColor? {
        get {
            if let color = objc_getAssociatedObject(self, &selectedColorHandle) as? UIColor {
                return color
            }
            return nil
        }
        set {
            if let color = newValue {
                self.setBackgroundColor(color, for: .selected)
                objc_setAssociatedObject(self, &selectedColorHandle, color, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            } else {
                self.setBackgroundImage(nil, for: .selected)
                objc_setAssociatedObject(self, &selectedColorHandle, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
}

See all the items in my Swift Tool Belt series

  1. Adding a Border, Corner Radius, and Shadow to a UIView
  2. Extending Date
  3. Extending UILabel
  4. Extending UITableViewController
  5. Adding a Gradient UIButton
  6. Extending UIFont
  7. Extending UIBarButtonItem
  8. Extending UIButton with Background Color for State