I have been playing with mesh gradients for over a month now. Let me take a pause, and focus on..drum rolls...mixing colors!

I wondered how digital colors blend. That curiousity led me to create a fun little game using SwiftUI's new color mixing feature. Let me walk you through it!

During WWDC 2024, SwiftUI introduced a new method called mix(with:by:in:) on Color that lets you blend two colors together.

I tried out some basic examples but was not happy enough to write a post about it. Then, as someone who has created a gradient and color game, the next step was probably obvious: what better way to understand this than by making a game?

How the Game Works

You see a target color, and your job is to mix two given colors to match it. Sounds simple, right? Well, getting it close to 100 is much trickier than you might think!

The game uses three main colors. There is the target color, the one that you aiming to mix. Then there are two base colors. You mix these colors to try and match the target with a slider.

The slider represents the mix ratio between the two base colors. Slide it all the way left, and you get 100% of the first color. All the way to the right and that is 100% of the second color. Anywhere in between gives you a mix of both.

0:00
/0:23

Mixing

The heart of the game is this line using SwiftUI's new method:

mixedColor = baseColor1.mix(with: baseColor2, by: mixRatio)

Let's break it down based on the initializer:

func mix(
    with rhs: Color,
    by fraction: Double,
    in colorSpace: Gradient.ColorSpace = .perceptual
) -> Color
  • baseColor1 is the first color. As it is a method on the Color itself, this is called self in the method definition.
  • baseColor2 is the second color, known as rhs in the method.
  • mixRatio is the value from our slider, which corresponds to fraction in the method.

The mix method blends these colors together. Here is what each part does:

  1. with: baseColor2 tells the method which color to mix with the first color.
  2. by: mixRatio determines how much of each color to use. If mixRatio is 0.3, you get 70% of the first color and 30% of the second.
  3. There is an optional third parameter, in:, which defaults to .perceptual. This decides how the colors are mixed.

The method returns a new Color which is a mix of the two input colors. We can think of it as a color cooking recipe: take some of color A, add some of color B, and you get a new color C.

Color Spaces

The color space gives us a choice of how to mix these colors. We can mix them in the "device" color space or the "perceptual" color space.

The device color space is like mixing paint by numbers. Straightforward but can sometimes give unexpected results. The perceptual color space is smarter. It tries to mix colors the way our eyes expect to see them mixed. Usually the better choice for smooth, natural-looking blends.

Moving Forward

Creating this game did taught me a lot. I learned how digital colors work and how to use this simple yet useful method to blend colors.

I will think more about this idea on how to make it addictive and then add it to Chroma Game for an iOS 18 release.

Give it a try. Build the game and play around with it. Here is the full code:

import SwiftUI

struct ColorMixingGame: View {
  @State private var targetColor: Color
  @State private var baseColor1: Color
  @State private var baseColor2: Color
  @State private var mixRatio = 0.5
  @State private var currentScore: Double = 0

  init() {
    let (target, color1, color2) = Self.generateGameColors()
    _targetColor = State(initialValue: target)
    _baseColor1 = State(initialValue: color1)
    _baseColor2 = State(initialValue: color2)
  }

  var mixedColor: Color {
    baseColor1.mix(with: baseColor2, by: mixRatio)
  }
}

extension ColorMixingGame {
  var body: some View {
    VStack(spacing: 20) {
      Text("Color Mixing Game")
        .font(.title)
        .fontWidth(.expanded)

      ColorLabel(color: targetColor, text: "Target")
      ColorLabel(color: mixedColor, text: "Your Mix")

      HStack {
        RoundedRectangle(cornerRadius: 12)
          .fill(baseColor1)

        RoundedRectangle(cornerRadius: 12)
          .fill(baseColor2)
      }

      Slider(value: $mixRatio)

      Text("Score: \(currentScore, specifier: "%.2f")%")
        .font(.headline)
        .fontWidth(.expanded)

      HStack {
        Button("Check Score") {
          currentScore = calculateScore(target: targetColor, mixed: mixedColor)
        }
        .fontWidth(.expanded)
        .buttonStyle(.borderedProminent)

        Button("New Colors") {
          resetGame()
        }
        .fontWidth(.expanded)
        .buttonStyle(.bordered)
      }
    }
    .padding()
  }
}

extension ColorMixingGame {
  private static func generateGameColors() -> (target: Color, base1: Color, base2: Color) {
    let color1 = Color.random()
    let color2 = Color.random()
    let targetMixRatio = Double.random(in: 0...1)
    let targetColor = color1.mix(with: color2, by: targetMixRatio)
    return (targetColor, color1, color2)
  }

  private func calculateScore(target: Color, mixed: Color) -> Double {
    let targetComponents = target.rgbComponents()
    let mixedComponents = mixed.rgbComponents()

    let componentDifferences = zip(targetComponents, mixedComponents).map { abs($0 - $1) }
    let averageDifference = componentDifferences.reduce(0, +) / Double(componentDifferences.count)

    return (1 - averageDifference) * 100
  }

  private func resetGame() {
    let (target, color1, color2) = Self.generateGameColors()
    targetColor = target
    baseColor1 = color1
    baseColor2 = color2
    mixRatio = 0.5
    currentScore = 0
  }
}

#Preview {
  ColorMixingGame()
}

extension Color {
  static func random() -> Color {
    Color(
      red: Double.random(in: 0...1),
      green: Double.random(in: 0...1),
      blue: Double.random(in: 0...1)
    )
  }

  func rgbComponents() -> [Double] {
    let uiColor = UIColor(self)
    var red: CGFloat = 0
    var green: CGFloat = 0
    var blue: CGFloat = 0
    var alpha: CGFloat = 0

    uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
    return [Double(red), Double(green), Double(blue)]
  }
}

struct ColorLabel: View {
  let color: Color
  let text: String

  var body: some View {
    ZStack(alignment: .top) {
      RoundedRectangle(cornerRadius: 12)
        .fill(color)

      Text(text)
        .font(.callout)
        .fontWidth(.expanded)
        .padding(.horizontal, 8)
        .padding(.vertical, 4)
        .background(
          UnevenRoundedRectangle(
            cornerRadii: .init(
              topLeading: 0,
              bottomLeading: 8,
              bottomTrailing: 8,
              topTrailing: 0
            ),
            style: .continuous
          )
          .fill(.ultraThinMaterial)
        )
    }
  }
}

Happy color mixing!

Astro Affiliate.

Astro (Affiliate)

Find the right keywords for your app and climb the App Store rankings. Improve your app visibility and increase your revenue with Astro. The first App Store Optimization tool designed for indie developers!

Tagged in: