Build a harmony chosen at runtime, such as from a picker in your theme settings, with .harmony(_:). You pass a ColorHarmony and get its colors back in a fixed order, your own color included.
struct HarmonySwatches: View {
let harmony: ColorHarmony
var body: some View {
let families = brand.harmony(harmony)
HStack(spacing: 4) {
ForEach(families.indices, id: \.self) { index in
Rectangle()
.fill(families[index].backgroundTertiary) // _200 in light mode, _700 in dark
.frame(width: 32, height: 32)
}
}
}
}
VStack(alignment: .leading) {
HarmonySwatches(harmony: .triad) // 167°, 287° and 47°
HarmonySwatches(harmony: .splitComplement(spread: .degrees(20))) // 167°, 327° and 7°
}Pick a harmony and a color below; the Swift under the result updates as you go.
Every OKLCH hue at this color's lightness
let color = Color.proBlue._400.toColor()
color.triadLetting people choose#
ColorHarmony is Hashable, so it can be a picker's selection. Include your own spreads and offsets as separate options:
struct HarmonyPicker: View {
@State private var harmony: ColorHarmony = .triad
var body: some View {
Picker("Harmony", selection: $harmony) {
Text("Triad").tag(ColorHarmony.triad)
Text("Square").tag(ColorHarmony.square)
Text("Split complement").tag(ColorHarmony.splitComplement())
}
}
}The harmonies you can pass#
Each case lists its hues as offsets around the wheel, in degrees from yours, in the order the colors come back:
| Case | hueOffsets | The same as |
|---|---|---|
.complement | 0, 180 | complement, with your color first |
.triad | 0, 120, 240 | triad |
.square | 0, 90, 180, 270 | square |
.tetrad(offset:) | 0, offset, 180, 180 + offset | tetrad(offset:) |
.splitComplement(spread:) | 0, 180 − spread, 180 + spread | splitComplement(spread:) |
.analogous(count:spread:) | count offsets spread apart, centered on 0 | analogous(count:spread:) |
Read the offsets in code when you need them, such as to label a hue wheel:
let offsets = ColorHarmony.splitComplement(spread: .degrees(20)).hueOffsets // [0, 160, 200]How it behaves#
An offset of 0 returns your color exactly as it was. Every other offset turns it with rotateHue(by:), so a palette color lands on the same stop at its new hue, with the same contrast. On a ProTheme, you get whole families at the stop you started from. Gray has no hue, so every harmony of a gray is gray.
Color and ProTheme read the same ColorHarmony, so they always agree: the complement of brand at _600 is the same color whichever route you take. The one difference from the named properties is .complement: the property returns only the turned color, while harmony(.complement) returns both. Color theory covers which harmony suits which job.
API#
public extension Color {
func harmony(_ harmony: ColorHarmony) -> [Color]
}
public extension ProTheme {
func harmony(_ harmony: ColorHarmony) -> [ProTheme]
}
public enum ColorHarmony: Hashable {
case complement
case triad
case tetrad(offset: Angle = .degrees(60))
case square
case splitComplement(spread: Angle = .degrees(30))
case analogous(count: Int = 3, spread: Angle = .degrees(30))
public var hueOffsets: [Double] { get }
}| Parameter | Type | Default | What other values do |
|---|---|---|---|
harmony | ColorHarmony | none | Which harmony to build. Each case's own defaults match the named functions. |
It returns one color or family per entry in hueOffsets, in that order.
Sources#
- ColorTokensKit source: ColorHarmony.swift, which defines every harmony's offsets in one place, and Color+Harmonies.swift and ProTheme+Harmonies.swift, which build the colors.
See also
- Color theory Pair stops that always read, and pick secondary hues that match your brand, in both modes.
- .triad Secondary and tertiary colors for tags and categories, at the same contrast as your brand.
- .splitComplement(spread:) Two accents that stand apart from your brand, softer than its opposite, at the same contrast.
- .rotateHue(by:) Make an accent or a whole second theme from your brand color, at the same lightness.
- Setting up themes Recolor a view, a screen or your whole app from one value, with the same contrast in every hue.