May 25, 2023

Calculating Color Contrast Ratio in Swift

When it comes to dynamic templating of apps sometimes it’s helpful to determine at runtime if two colos have enough constrast so that one can be the background color and the other the foreground color. The W3C defines in it’s Web Content Accessibility Guidelines (WCAG) 2.0 a formula to find this ratio. Let’s take a look on how to implement this calculation using a Swift UIColor extension.

We start by creating a simple UIColor extension:

import UIKit

extension UIColor {

}

We then implement the logic that returns the relative luminance of a color, according to the W3C specification. This is in practice a Swift computed varibale in the UIColor class:

var luminance: CGFloat {
    let RED: CGFloat = 0.2126
    let GREEN: CGFloat = 0.7152
    let BLUE: CGFloat = 0.0722
    let GAMMA: CGFloat = 2.4
    
    var r: CGFloat = 0
    var g: CGFloat = 0
    var b: CGFloat = 0
    self.getRed(&r, green: &g, blue: &b, alpha: nil)
    
    let a = [r, g, b]
        .map({ v in
            return v <= 0.03928 ? v / 12.92 : pow((v + 0.055) / 1.055, GAMMA)
        })
    
    return a[0] * RED + a[1] * GREEN + a[2] * BLUE
}

With the luminance property calculated we can then implement the function that calculates the contrast ratio:

func contrastTo(color: UIColor) -> CGFloat {
    let lum1 = self.luminance
    let lum2 = color.luminance
    let brightest = max(lum1, lum2)
    let darkest = min(lum1, lum2)
    return (brightest + 0.05) / (darkest + 0.05)
}

So what is the threshold that is considered “having enough” contrast ratio? Well according to W3C it actually depends on the font size of the foreground color. So we can add a nice last little helper function that returns a boolean for a given color and font style.

func hasEnoughContrast(toColor color: UIColor, forTextSize size: CGFloat = 16, weight: UIFont.Weight = .regular) -> Bool {
    let contrast = self.contrastTo(color: color)
    switch weight {
    case .medium, .semibold, .bold, .heavy, .black:
        return size >= 14 && contrast >= 3
    default:
        return contrast >= 4.5
    }
}

Examples

let red = UIColor.red
print(red.luminance)

// print(red.luminance) --> 0.2126
let white = UIColor.white
let alto = UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)

let ratio = white.contrastTo(color: alto)
print(ratio)

// print(ratio) --> 1.453401544312084
let black = UIColor.black;
let hasEnoughContrast = black.hasEnoughContrast(toColor: .white, forTextSize: 20, weight: .bold);
print(hasEnoughContrast)

// print(hasEnoughContrast) --> true

Complete Extension