Exploring SwiftUI: Animating Mesh Gradient with Colors in iOS 18

I have been working with the new MeshGradient structure in SwiftUI for a month now, and feels like I barely scratched the surface. I am exploring animating the gradients, and we are going to create an animated mesh gradient using only colors. It is easier than you might think, and the results are gorgeous, as it should be. Let us dive in!

What is a Mesh Gradient?

What exactly is a mesh gradient? It is a type of gradient that uses multiple colors arranged in a grid. They are more complex and flexible version of a regular linear gradient.

  • In a 2x2 grid, you are somewhat limited. The color points are stuck at the edges of the shape.
  • With a 3x3 grid (like we are using in this example), you have a point in the center that you can move around, giving you more control over how colors blend.
  • In a 4x4 grid, you have even more flexibility. There are 4 points in the middle that you can adjust, letting you create more complex color transitions and even tangent-like effects like in the gradient below!

The more points you have, the more organic the gradient can become. For a simple example, I am using a 3x3 grid.

Setting Up Our Colors and Points

We start by defining the colors and points. Here is what that looks like:

private let colors: [Color] = [
  Color(red: 1.00, green: 0.42, blue: 0.42),
  Color(red: 1.00, green: 0.55, blue: 0.00),
  Color(red: 1.00, green: 0.27, blue: 0.00),

  Color(red: 1.00, green: 0.41, blue: 0.71),
  Color(red: 0.85, green: 0.44, blue: 0.84),
  Color(red: 0.54, green: 0.17, blue: 0.89),

  Color(red: 0.29, green: 0.00, blue: 0.51),
  Color(red: 0.00, green: 0.00, blue: 0.55),
  Color(red: 0.10, green: 0.10, blue: 0.44)
]

private let points: [SIMD2<Float>] = [
  SIMD2<Float>(0.0, 0.0), SIMD2<Float>(0.5, 0.0), SIMD2<Float>(1.0, 0.0),
  SIMD2<Float>(0.0, 0.5), SIMD2<Float>(0.5, 0.5), SIMD2<Float>(1.0, 0.5),
  SIMD2<Float>(0.0, 1.0), SIMD2<Float>(0.5, 1.0), SIMD2<Float>(1.0, 1.0)
]

SIMD stands for Single Instruction/Multiple Data. It is an efficient way to perform operations on multiple data points at once.

  1. SIMD2<Float> represents 2D coordinates (x, y) using floating-point numbers.
  2. Each SIMD2<Float> in the array represents a point in the 3x3 grid.
  3. The values range from 0.0 to 1.0, representing relative positions in the view.
  4. Using SIMD can lead to better performance, especially when doing calculations with these points.

I have chosen a set of vibrant colors for the gradient. The points array defines where these colors will be placed in the mesh. The first point starts from the top left corner, with the last one representing the bottom right corner. For 3x3 grid, we have 9 points total, 4x4 grid will have 16 points, and so on.

Creating the Mesh Gradient

Now, let us look at how we actually create and animate the gradient:

var body: some View {
  TimelineView(.animation) { timeline in
    MeshGradient(
      width: 3,
      height: 3,
      locations: .points(points),
      colors: .colors(animatedColors(for: timeline.date)),
      background: .black,
      smoothsColors: true
    )
  }
  .ignoresSafeArea()
}

Breaking down MeshGradient initializer parameter by parameter:

  1. width and height: These define the dimensions of the gradient mesh. In our example, I am using a 3x3 grid, so both width and height are 3.
  2. locations: This is an array of points where the colors will be placed.
  3. colors: This is an array of colors that correspond to each location in our mesh. The number of colors should match the number of locations (width x height).
  4. background: This is the color that fills any area outside the defined mesh.
  5. smoothsColors: This determines whether the color interpolation between points is smooth (cubic) or not. I like to set this to true for a more smooth look.

Also, I am using a TimelineView here. This view updates regularly, allowing to create smooth animations.

Animating the Colors

The wizard magic happens in the colors parameter. We are calling a method animatedColors(for:) and passing in the current date from the timeline. This is how we shift the hue of color over time:

private func animatedColors(for date: Date) -> [Color] {
  let phase = CGFloat(date.timeIntervalSince1970)

  return colors.enumerated().map { index, color in
    let hueShift = cos(phase + Double(index) * 0.3) * 0.1
    return shiftHue(of: color, by: hueShift)
  }
}

For each color, we calculate a hue shift using a cosine function. I like the cosine method over sine function. sin sounds like a sinner function.

This creates a smooth, wave-like shift for the colors over time.

Shifting the Hue

Finally, we shift the hue of the colors:

private func shiftHue(of color: Color, by amount: Double) -> Color {
  var hue: CGFloat = 0
  var saturation: CGFloat = 0
  var brightness: CGFloat = 0
  var alpha: CGFloat = 0

  UIColor(color).getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)

  hue += CGFloat(amount)
  hue = hue.truncatingRemainder(dividingBy: 1.0)

  if hue < 0 {
    hue += 1
  }

  return Color(hue: Double(hue), saturation: Double(saturation), brightness: Double(brightness), opacity: Double(alpha))
}

This function takes a color and a shift amount. It extracts the hue, saturation, brightness, and alpha values from the color by converting it into a UIColor first. Then it adjusts the hue by the given amount, making sure it stays within the valid range of 0 to 1. Finally, it creates a new Color with the shifted hue.

Moving Forward

When you run the code, you see a beautiful, smoothly animated mesh gradient filling your screen!

0:00
/0:12

Go berserk. Animate it with positions, too. Play around with different colors, grid sizes, and animation speeds to create your own unique effects.

Exploring SwiftUI: Animating Mesh Gradient on Text in iOS 18
Discover the mesh gradients in SwiftUI! This post explores how to animate MeshGradient on text, creating stunning visual effects. Learn to stretch and mask mesh gradients, bringing your SwiftUI text to life with flowing colors.

Here is the full code to copy and explore in your project:

struct AnimatedColorsMeshGradientView: View {
  private let colors: [Color] = [
    Color(red: 1.00, green: 0.42, blue: 0.42),
    Color(red: 1.00, green: 0.55, blue: 0.00),
    Color(red: 1.00, green: 0.27, blue: 0.00),

    Color(red: 1.00, green: 0.41, blue: 0.71),
    Color(red: 0.85, green: 0.44, blue: 0.84),
    Color(red: 0.54, green: 0.17, blue: 0.89),

    Color(red: 0.29, green: 0.00, blue: 0.51),
    Color(red: 0.00, green: 0.00, blue: 0.55),
    Color(red: 0.10, green: 0.10, blue: 0.44)
  ]

  private let points: [SIMD2<Float>] = [
    SIMD2<Float>(0.0, 0.0), SIMD2<Float>(0.5, 0.0), SIMD2<Float>(1.0, 0.0),
    SIMD2<Float>(0.0, 0.5), SIMD2<Float>(0.5, 0.5), SIMD2<Float>(1.0, 0.5),
    SIMD2<Float>(0.0, 1.0), SIMD2<Float>(0.5, 1.0), SIMD2<Float>(1.0, 1.0)
  ]
}

extension AnimatedColorsMeshGradientView {
  var body: some View {
    TimelineView(.animation) { timeline in
      MeshGradient(
        width: 3,
        height: 3,
        locations: .points(points),
        colors: .colors(animatedColors(for: timeline.date)),
        background: .black,
        smoothsColors: true
      )
    }
    .ignoresSafeArea()
  }
}

extension AnimatedColorsMeshGradientView {
  private func animatedColors(for date: Date) -> [Color] {
    let phase = CGFloat(date.timeIntervalSince1970)

    return colors.enumerated().map { index, color in
      let hueShift = cos(phase + Double(index) * 0.3) * 0.1
      return shiftHue(of: color, by: hueShift)
    }
  }

  private func shiftHue(of color: Color, by amount: Double) -> Color {
    var hue: CGFloat = 0
    var saturation: CGFloat = 0
    var brightness: CGFloat = 0
    var alpha: CGFloat = 0

    UIColor(color).getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)

    hue += CGFloat(amount)
    hue = hue.truncatingRemainder(dividingBy: 1.0)

    if hue < 0 {
      hue += 1
    }

    return Color(hue: Double(hue), saturation: Double(saturation), brightness: Double(brightness), opacity: Double(alpha))
  }
}

#Preview {
  AnimatedColorsMeshGradientView()
}

Happy meshing!