Snippet of the Week: Lighter and Darker Colors 🎨


Especially when you’re prototyping user interfaces, you tend to use quite a bit of colors, especially colors that are derived from one another. You might have defined a base color; for example UIColor.purple. But afterwards, you might also need a darker version for shadows, or a lighter version for emphasis. Instead of opening an color picker and searching around for awhile, consider how easy it would be if you could just do; UIColor.purple.darkened or UIColor.purple.lightened. Let’s do this!

UIColor has a couple of convenience initializers; one of which is

UIColor(hue: CGFloat, saturation: CGFloat, brightness: CGFloat, alpha: CGFloat)

One of the parameters; brightness certainly looks useful. But, most of our colors are defined in the RGB color space, so how do we get our base color’s HSB values?

public extension UIColor {
    public func hsba() -> (hue: CGFloat, saturation: CGFloat, brightness: CGFloat, alpha: CGFloat)? {
        var hue: CGFloat = .nan, saturation: CGFloat = .nan, brightness: CGFloat = .nan, alpha: CGFloat = .nan
        guard self.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) else {
            return nil
        }
        return (hue: hue, saturation: saturation, brightness: brightness, alpha: alpha)
    }
}

Okay, so now we have a tuple with our values. Using this, we can alter the brightness easily enough and use it to create a new color.

public extension UIColor {
    public func changedBrightness(byPercentage perc: CGFloat) -> UIColor? {
        if perc == 0 {
            return self.copy() as? UIColor
        }
        guard let hsba = hsba() else {
            return nil
        }
        let percentage: CGFloat = min(max(perc, -1), 1)
        let newBrightness = min(max(hsba.brightness + percentage, -1), 1)
        return UIColor(hue: hsba.hue, saturation: hsba.saturation, brightness: newBrightness, alpha: hsba.alpha)
    }
}

That’s looking good. But changedBrightness(byPercentage:) is a lot of typing. Time for convenience methods!

public extension UIColor {
    public func lightened(byPercentage percentage: CGFloat = 0.1) -> UIColor? {
        return changedBrightness(byPercentage: percentage)
    }

    public func darkened(byPercentage percentage: CGFloat = 0.1) -> UIColor? {
        return changedBrightness(byPercentage: -percentage)
    }
}

Yes, now it’s easy to create derived colors that vary in brightness;

let color = UIColor.purple
let colors = [ color.darkened(byPercentage: 0.2), color.darkened(), color, color.lightened, color.lightened(byPercentage: 0.2) ]

Five variations on purple

Some interesting tidbits;

  • We use &someVariable to fill in a previously defined variable from another method. Kind of ugly, but some UIColor methods we still have to live with.
  • If we don’t actually change the color (perc == 0), we return a copy early.
  • The conversion to HSB might fail, so we return optionals. This is probably the most annoying bit of the code, since we’re unlikely to ever run into that. However, making the methods throw seems like overkill. Any suggestions?
  • Since the brightness has a range from 0 to 1, we use a max(min(_,-1)_,1) combo to make sure the values don’t stray beyond that.
  • I’m not terribly happy with the naming of lightned and darkened. I would prefer, lighter and darker, but that doesn’t mesh well with Swift’s naming conventions. I might revisit this soon though.

You can find an entire playground in this gist.

If you think the snippet is useful – or have an improvement – send me tweet or a toot!