Skip to content

Commit b85b1a1

Browse files
Mark Pospeselmpospese
authored andcommitted
[Issue 42] Calculate contrast ratios for transparent colors
1 parent 271bcdd commit b85b1a1

File tree

2 files changed

+70
-3
lines changed

2 files changed

+70
-3
lines changed

Sources/YCoreUI/Extensions/UIKit/UIColor+WCAG.swift

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,31 @@ extension UIColor {
9191
/// - Returns: the contrast ratio between the two colors ranging from 1.0 (identical) to 21.0
9292
/// (the contrast between pure white and pure black)
9393
public func contrastRatio(to otherColor: UIColor) -> CGFloat {
94-
let ourLuminance = self.luminance
95-
let theirLuminance = otherColor.luminance
94+
let ourAlpha = self.rgbaComponents.alpha
95+
let theirAlpha = otherColor.rgbaComponents.alpha
96+
guard ourAlpha == 1.0 || theirAlpha == 1.0 else {
97+
// can't calculate a contrast between two transparent colors
98+
if YCoreUI.isLoggingEnabled {
99+
YCoreUI.colorLogger.warning(
100+
"Transparent color \(self) cannot calculate contrast ratio to other transparent color \(otherColor)."
101+
)
102+
}
103+
return .nan
104+
}
105+
106+
var color1 = self
107+
var color2 = otherColor
108+
109+
if ourAlpha < 1.0 {
110+
// if color1 is partially transparent, blend it with color2 before evaluating
111+
color1 = color2.blended(by: ourAlpha, with: color1)
112+
} else if theirAlpha < 1.0 {
113+
// if color2 is partially transparent, blend it with color1 before evaluating
114+
color2 = color1.blended(by: theirAlpha, with: color2)
115+
}
116+
117+
let ourLuminance = color1.luminance
118+
let theirLuminance = color2.luminance
96119
let lighterColor = min(ourLuminance, theirLuminance)
97120
let darkerColor = max(ourLuminance, theirLuminance)
98121
return 1 / ((lighterColor + 0.05) / (darkerColor + 0.05))

Tests/YCoreUITests/Extensions/UIKit/UIColor+WCAGTests.swift

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ final class UIColorWCAGTests: XCTestCase {
5353
XCTAssert(
5454
color1.isSufficientContrast(to: color2, context: $0.context, level: .AA),
5555
String(
56-
format: "#%@ vs #%@ ratio = %.02f under %@ Mode%@",
56+
format: "#%@ %.2f vs #%@ %.2f ratio = %.02f under %@ Mode%@",
5757
color1.rgbDisplayString(prefix: "#"),
58+
color1.rgbaComponents.alpha,
5859
color2.rgbDisplayString(prefix: "#"),
60+
color2.rgbaComponents.alpha,
5961
color1.contrastRatio(to: color2),
6062
traits.userInterfaceStyle == .dark ? "Dark" : "Light",
6163
traits.accessibilityContrast == .high ? " Increased Contrast" : ""
@@ -135,6 +137,48 @@ final class UIColorWCAGTests: XCTestCase {
135137
XCTAssertEqual(UIColor.black.contrastRatio(to: .white), 21.0)
136138
}
137139

140+
func test_contrastRatio_deliversRatioForOneTransparentColor() {
141+
let color = UIColor(rgb: 0x1B0B99)
142+
let color75 = color.withAlphaComponent(0.75)
143+
let color50 = color.withAlphaComponent(0.50)
144+
let color25 = color.withAlphaComponent(0.25)
145+
let white = UIColor.white
146+
let black = UIColor.black
147+
148+
// contrast against white
149+
contrastRatiosEqual(ratio1: color.contrastRatio(to: white), ratio2: 13.51)
150+
contrastRatiosEqual(ratio1: color75.contrastRatio(to: white), ratio2: 7.12)
151+
contrastRatiosEqual(ratio1: white.contrastRatio(to: color75), ratio2: 7.12)
152+
contrastRatiosEqual(ratio1: color50.contrastRatio(to: white), ratio2: 3.30)
153+
contrastRatiosEqual(ratio1: white.contrastRatio(to: color50), ratio2: 3.30)
154+
contrastRatiosEqual(ratio1: color25.contrastRatio(to: white), ratio2: 1.71)
155+
contrastRatiosEqual(ratio1: white.contrastRatio(to: color25), ratio2: 1.71)
156+
157+
// contrast against black
158+
contrastRatiosEqual(ratio1: color.contrastRatio(to: black), ratio2: 1.55)
159+
contrastRatiosEqual(ratio1: color75.contrastRatio(to: black), ratio2: 1.31)
160+
contrastRatiosEqual(ratio1: black.contrastRatio(to: color75), ratio2: 1.31)
161+
contrastRatiosEqual(ratio1: color50.contrastRatio(to: black), ratio2: 1.15)
162+
contrastRatiosEqual(ratio1: black.contrastRatio(to: color50), ratio2: 1.15)
163+
contrastRatiosEqual(ratio1: color25.contrastRatio(to: black), ratio2: 1.05)
164+
contrastRatiosEqual(ratio1: black.contrastRatio(to: color25), ratio2: 1.05)
165+
}
166+
167+
func test_contrastRatio_deliversNanForTwoTransparentColors() {
168+
let color = UIColor(rgb: 0x1B0B99)
169+
let color75 = color.withAlphaComponent(0.75)
170+
let color50 = color.withAlphaComponent(0.50)
171+
172+
XCTAssertLessThan(color75.rgbaComponents.alpha, 1.0)
173+
XCTAssertLessThan(color50.rgbaComponents.alpha, 1.0)
174+
XCTAssertTrue(color75.contrastRatio(to: color50).isNaN)
175+
YCoreUI.isLoggingEnabled = false
176+
XCTAssertTrue(color50.contrastRatio(to: color75).isNaN)
177+
XCTAssertFalse(color50.isSufficientContrast(to: color75))
178+
XCTAssertFalse(color75.isSufficientContrast(to: color50))
179+
YCoreUI.isLoggingEnabled = true
180+
}
181+
138182
private func contrastRatiosEqual(ratio1: CGFloat, ratio2: CGFloat) {
139183
XCTAssertEqual(round(ratio1 * 100) / 100, round(ratio2 * 100) / 100)
140184
}

0 commit comments

Comments
 (0)