Skip to content

26.9.5

Latest

Choose a tag to compare

@markbattistella markbattistella released this 05 Sep 03:47

Breaking

  • highestRatedContrastLevel, aaaContrastLevel, aaContrastLevel, aaLargeContrastLevel now return Color?. They used to silently return .white when nothing in the candidate set actually met the requested level. Now they return nil — check it and pick a fallback deliberately.
  • ContrastLevel.ratio (internal (min, max) tuple) removed. Replaced by ContrastLevel.minimumRatio: Double. There's no upper bound to migrate to — see next point.
  • Contrast levels are floors, not bands. AA no longer rejects a candidate for being too readable (previously capped at 6.99), and AA Large is no longer capped at 4.49. Meeting AAA now also means meeting AA and AA Large, per WCAG. aaContrastLevel/aaLargeContrastLevel may now return a different (higher-contrast, more correct) result than before.
  • The lightness ramp (level(_:for:), level(_:), shorthand properties) now derives from a true RGB→HSL conversion, not HSB read via getHue(_:saturation:brightness:). Any saturated color's generated shades will differ slightly from previous versions — this was a real bug (HSB brightness ≠ HSL lightness), not a style change. Grayscale colors are unaffected.
  • Luminance and contrast now convert to sRGB before computing anything. Display P3 asset colors — Xcode's default — previously had their raw components read directly and pushed through the sRGB-only WCAG formula, silently producing wrong numbers that grew with saturation. Every ratio and derived color involving a wide-gamut color will change to the correct value.

Added

  • Color.relativeLuminance: Double — WCAG 2.1 relative luminance, 0...1.
  • Color.contrastRatio(with:) -> Double — the actual ratio between two colors, 1...21. This is the one thing the package couldn't do before: answer "what is the contrast ratio," not just "pick me a color."
  • Color.meets(_:against:) -> Bool — direct pass/fail check against a ContrastLevel.
  • Color.flattened(over:) -> Color — alpha-composites a translucent color over an opaque backdrop. WCAG contrast is only defined for opaque colors; use this before measuring contrast on anything with alpha < 1.
  • Color.mostReadableForeground(from:meeting:) -> Color? — picks the best-contrasting color from an arbitrary candidate list that still meets a given level, instead of only ever choosing from [.white, .black] + the built-in ramp.
  • Trait-aware overloads for all of the above — compatibleWith: UITraitCollection (UIKit) / compatibleWith: NSAppearance (AppKit) — so contrast can be measured against a specific appearance (light/dark, and Increase Contrast via accessibilityContrast) instead of whatever's ambient at the call site. Ambient resolution is unpredictable in tests and wrong outside the screen the call happens to run on.
  • ContrastLevel.minimumRatio: Double.

Fixed

  • Alpha was previously ignored entirely when computing luminance — a translucent color returned a confidently wrong number. relativeLuminance/contrastRatio/flattened(over:) now handle it: asserts (debug-only) if you call luminance/contrast on a non-opaque color without compositing it first.
  • The WCAG ratio formula was implemented three times across the codebase (production code, a DEBUG-only preview helper, and effectively duplicated in the test suite) — and the tests were exercising the DEBUG-only copy, not the one that ships in Release. There is now exactly one implementation, and it's the one both Release builds and tests use.
  • Dead @available(iOS 14.0, macOS 11.0, …) annotations removed — the package has required iOS 17 / macOS 14 since 26.2.20; these were unreachable and misleading.

Improved

  • ColorLevelStyle now conforms to Sendable.
  • Reindented ContrastLevel.swift and PreviewTesting.swift to match the rest of the codebase (4-space).
  • Test suite expanded from 3 tests (exercising only internal, DEBUG-only code) to 19 tests covering the full public API, including a P3-vs-sRGB regression test verified against Apple's own CGColor conversion, and trait/appearance-aware resolution verified against dynamic color fixtures.

See the README migration guide for before/after code and full detail on every change above.