This month’s code-alongs are dedicated to introduce you to some visual effects: creating mesh art by using MeshGradient
and creating a cool ripple effect by using Apple’s high-performance graphics framework Metal.
Let’s get started with some Mesh Art which will look the following:

This is an extension of the WWDC24 talk Create custom visual effects with SwiftUI where a fixed mesh gradient is being introduced. You can download the WWDC project here: https://developer.apple.com/documentation/SwiftUI/Creating-visual-effects-with-SwiftUI
Let’s get started!
Step 1: Set up your project
- Open Xcode: Launch Xcode and select Create a new Xcode project.
- Choose Template: Select App under the iOS tab and click Next.
- Name Your Project: Enter a name for your project, like
MeshArt
.
Choose SwiftUI for the interface and Swiftfor the language. Click Next, and then save your project.
When you open your project, you’ll see the already familiar standard code presenting a globe and the Text “Hello, world!” in the ContentView.swift
. As last time, this will be our “Home”-View which will present all the other views. This time we will be using a NavigationStack
. But let’s start with a static Mesh Gradient view presented at WWDC24:
Step 2: A static Mesh Gradient view
Create a new SwiftUI file. Go to File - New - File from Template...
or simply press cmd + N
, choose iOS Template
and here SwiftUI View
, click Next
, choose StaticMeshGradientView
as the name of the new view, ensure that MeshArt
is selected under Group
, check the tick box for Targets: MeshArt
, click Create
.
Okay, let’s create the same MeshGradient as showcased at WWDC24. But what is a MeshGradient?
A MeshGradient is a two-dimensional gradient defined by a 2D grid of positioned colors.
A MeshGradient is a way to create a complex, multi-dimensional gradient by defining a grid (or mesh) of control points, each with its own color. Instead of just a linear or radial gradient, you supply a grid of points—say, a 3×3 grid—and assign colors to each control point. The system then interpolates between these colors to produce a smooth transition across the entire mesh.
In many custom implementations, MeshGradient is powered by a Metal shader to render the effect efficiently.
To create a mesh gradient, replace Text("Hello, World!")
with the following code:
MeshGradient(
width: 3,
height: 3,
points: [
[0.0, 0.0], [0.5, 0.0], [1.0, 0.0],
[0.0, 0.5], [0.9, 0.3], [1.0, 0.5],
[0.0, 1.0], [0.5, 1.0], [1.0, 1.0]
],
colors: [
.black,.black,.black,
.blue, .blue, .blue,
.green, .green, .green
]
)
.ignoresSafeArea(edges: .all)
This snippet sets up a static mesh gradient by:
- Defining a 3×3 grid with specific grid points.
- Associating a solid color with each of those control points. Here, each row gets the same color.
- Creating smooth color transitions via interpolation between these points.
- Expanding the gradient to fill the entire screen.
MeshGradient
uses a normalized coordinate system where 0
and 1
represent the start and end of each axis. Think of 1
as 100%
of the respective axis. Point [0.0, 0.0]
is the top-left corner, [1.0, 0.0]
the top-right, [0.0, 1.0]
bottom-left, and [1.0, 1.0]
the bottom-right corner.
MeshGradient
uses a normalized coordinate system where 0
and 1
represent the start and end of each axis. Think of 1
as 100%
of the respective axis. Point [0.0, 0.0]
is the top-left corner, [1.0, 0.0]
the top-right, [0.0, 1.0]
bottom-left, and [1.0, 1.0]
the bottom-right corner.
Did you notice the second row when defining the grid is not as expected? A regular 3×3 grid is the following:
[0.0, 0.0], [0.5, 0.0], [1.0, 0.0],
[0.0, 0.5], [0.5, 0.5], [1.0, 0.5],
[0.0, 1.0], [0.5, 1.0], [1.0, 1.0]
In our example, the middle point is defined by [0.9, 0.3]
instead of [0.5, 0.5]
, i.e. the “middle” point is shifted to the right and top which creates this “distorted” pattern. If you change this “middle” point to [0.5, 0.5]
, then you get a regular gradient, black at the top, blue in the middle, and green at the bottom with color transitions in between.
Of course, you can also assign a different color to each of those grid points.
Okay, let’s make it a little bit more interesting by applying a mesh gradient to a text.
Step 3: Create a “Pride” view
Create a new SwiftUI file. Go to File - New - File from Template...
or simply press cmd + N
, choose iOS Template
and here SwiftUI View
, click Next
, choose PrideView
as the name of the new view, ensure that MeshArt
is selected under Group
, check the tick box for Targets: MeshArt
, click Create
.
Adjust the Text("Hello, World!")
to
Text("Pride")
.font(.system(size: 144, weight: .bold))
We want to make the “Pride” pop in pride colors using a mesh gradient. There are 6 colors that form the pride flag: red, orange, yellow, green, blue, and purple. We want to create 6 stripes, i.e. we need to define 6 lines, each starting from the very left (x=0
), to the very right (x=1
). As the bottom grid points are [0,1]
and [1,1]
, we just need to vary the y
value for each line. Since we got 6 colors, we need to define 0.2
steps. Therefore, the mesh gradient looks the following:
MeshGradient(width: 2, height: 6, points: [
[0, 0], [1,0],
[0,0.2], [1,0.2],
[0,0.4], [1,0.4],
[0,0.6], [1,0.6],
[0,0.8], [1,0.8],
[0, 1], [1, 1]
], colors: [
.red, .red,
.orange, .orange,
.yellow, .yellow,
.green, .green,
.blue, .blue,
.purple, .purple
])
You can now apply this to the Text
view by using the .foregroundStyle
modifier:
Text("Pride")
.font(.system(size: 144, weight: .bold))
.foregroundStyle(
MeshGradient(width: 2, height: 6, points: [
[0, 0], [1,0],
[0,0.2], [1,0.2],
[0,0.4], [1,0.4],
[0,0.6], [1,0.6],
[0,0.8], [1,0.8],
[0, 1], [1, 1]
], colors: [
.red, .red,
.orange, .orange,
.yellow, .yellow,
.green, .green,
.blue, .blue,
.purple, .purple
])
)
That’s all it needs to create a Pride stripe color!
However, we can do better. If you want to apply different colors, different number of colors, you always need to type this code again and again and you need to adjust the grid points. As an example: If you want to create a circle with the same pride colors, you’ll need to write again:
Circle()
.foregroundStyle(
MeshGradient(width: 2, height: 6, points: [
[0, 0], [1,0],
[0,0.2], [1,0.2],
[0,0.4], [1,0.4],
[0,0.6], [1,0.6],
[0,0.8], [1,0.8],
[0, 1], [1, 1]
], colors: [
.red, .red,
.orange, .orange,
.yellow, .yellow,
.green, .green,
.blue, .blue,
.purple, .purple
])
)
.padding()
To simplify, let’s create a reusable stripe modifier with a variable number of colors:
Step 4: Create a reusable stripe modifier
We create a StripesModifier
which is defined as a ViewModifier
. In SwiftUI, a ViewModifier
lets you create a reusable piece of styling or behavior that you can apply to any view.
We want to provide an array of colors – i.e. need a constant that takes the provided colors. The mesh grid is defined as a 2xn
grid – 2
because we want to create stripes and for this we need to points, the left and the right end, and n
is the number of colors provided.
Below your struct PrideView
, please add the following (you can also create a new file for this):
struct StripesModifier: ViewModifier {
let colors: [Color]
func body(content: Content) -> some View {
let rows = colors.count
let points: [SIMD2<Float>] = {
if rows <= 1 {
return [SIMD2<Float>(0, 0), SIMD2<Float>(1, 0)]
} else {
return (0..<rows).flatMap { i -> [SIMD2<Float>] in
let y = Float(i) / Float(rows - 1)
return [SIMD2<Float>(0, y), SIMD2<Float>(1, y)]
}
}
}()
let duplicatedColors = colors.flatMap { [ $0, $0 ] }
return content
.foregroundStyle(
MeshGradient(
width: 2,
height: rows,
points: points,
colors: duplicatedColors
)
)
}
}
Let’s break it down:
The function signature
func body(content: Content) -> some View {
// ...
}
is part of the ViewModifier
protocol:
func body(content: Content)
: This method takes one parameter calledcontent
, which represents the original view that you want to modify. Think of it as the “base” view that you’re applying your changes to.-> some View
: This means that the function returns a view. Thesome View
part is an opaque return type, meaning it returns a view, but the exact type isn’t specified. This allows SwiftUI to work with complex view compositions without needing to know the precise type.
Inside this method, you define how to modify or wrap the original view (content
). For example, you might add padding, backgrounds, or any other view modifiers. The result is a new view that combines the original view and your modifications.
As an example:
func body(content: Content) -> some View {
content
.padding()
.background(Color.red)
}
takes the view it gets, adds some padding, puts a red background behind it, and then give back this new, decorated view.
In our StripesModifier
we get back a new view to which the mesh gradient has been applied with a [2, rows]
grid (where rows
is defined by the number of colors provided, i.e. colors.count
), defined points, and duplicatedColors
which are simply duplicating the given color – e.g. .red
– to .red, .red
so that we assign to the left and right end of our grid the same color.
You might wonder what SIMD2<Float>(0, 0)
means? Our points are represented by a 2-dimensional vector and this is what is required when working with graphics and shaders. And this is what SIMD2<Float>(0, 0)
creates, a two-dimensional vector (or point) with Float
values.
return content
.foregroundStyle(
MeshGradient(
width: 2,
height: rows,
points: points,
colors: duplicatedColors
)
)
This duplication is achieved by duplicatedColors = colors.flatMap { [ $0, $0 ] }
where $0
simply means the given argument.
How is the grid defined? The points
array is created to tell the gradient where each stripe begins and ends. If there’s only one color, it returns two points at the top. Otherwise, it creates two points for every row. The y
-coordinate of each point is calculated so the rows are evenly spaced between 0
(top) and 1
(bottom). Each row gets two points: one on the left (x=0
) and one on the right (x=1
).
With this we could apply the StripesModifier
modifier to our text view:
Text("Pride")
.modifier(StripesModifier(colors: [.red, .orange, .yellow, .green, .blue, .purple]))
However, this still looks a little bit cumbersome.
By wrapping the modifier in an extension, you can create a much cleaner and more descriptive API. Instead of using the generic .modifier
call, you get a method that clearly expresses what it does—applying a stripe gradient.
Therefore, please write:
extension View {
func stripes(_ colors: Color...) -> some View {
self.modifier(StripesModifier(colors: colors))
}
}
With this, you can create a pride colored text just by saying
Text("Pride")
.font(.system(size: 144, weight: .bold))
.stripes(.red, .orange, .yellow, .green, .blue, .purple)
(or any other colors and different number of colors) which is much cleaner and easier to read.
Of course, you can also apply this to our circle:
Circle()
.stripes(.red, .orange, .yellow, .green, .blue, .purple)
.padding()
That looks much easier and cleaner!
You can read about creating a modifier also here in Apple’s documentation: https://developer.apple.com/documentation/swiftui/view/modifier(_:)
With this experience on mesh gradients, let’s create some mesh art!
Step 5: MeshArt: A draggable Mesh Gradient view
We want to create a mesh gradient view with 3 colors at the top, middle, and bottom, respectively, which can be changed by the user. Little dots should indicate the grid points (here we want to use a [3x3]
grid) which can be dragged around by the user to see the impact. This might give you also a better “feeling” for the grid and how to set the points for the grid as you will be able to move them around freely.
To get started: Create a new SwiftUI file. Go to File - New - File from Template...
or simply press cmd + N
, choose iOS Template
and here SwiftUI View
, click Next
, choose DraggableMeshView
as the name of the new view, ensure that MeshArt
is selected under Group
, check the tick box for Targets: MeshArt
, click Create
.
We want to use the code from StaticMeshGradientView
we created earlier as a starting point. Therefore, please copy the full code here to our DraggableMeshView
.
To start with an undistorted grid, we’ll change the mid-point to [0.5, 0.5]
that gives an even mesh gradient from the top to the bottom.
First, let’s make the grid points draggable:
Define draggable points – var because they will be changed due to dragging
private var dragPoints: [CGPoint] = [
CGPoint(x: 0.0, y: 0.0), CGPoint(x: 0.5, y: 0.0), CGPoint(x: 1.0, y: 0.0),
CGPoint(x: 0.0, y: 0.5), CGPoint(x: 0.5, y: 0.5), CGPoint(x: 1.0, y: 0.5),
CGPoint(x: 0.0, y: 1.0), CGPoint(x: 0.5, y: 1.0), CGPoint(x: 1.0, y: 1.0)
]
Let’s define our colors as varables so that we can change them later:
@State private var topColor: Color = .black
@State private var middleColor: Color = .blue
@State private var bottomColor: Color = .green
Please adjust the MeshGradient
accordingly:
MeshGradient(
width: 3,
height: 3,
points: dragPoints.map { [Float($0.x), Float($0.y)] },
colors: [
topColor, topColor, topColor,
middleColor, middleColor, middleColor,
bottomColor, bottomColor, bottomColor
]
)
.ignoresSafeArea(edges: .all)
Great! All is still looking the same but is prepared for a variable setting.
Embed the MeshGradient
into a GeometryReader
and ZStack
to overlay with dragabble points
GeometryReader { geometry in
ZStack {
MeshGradient( ... )
}
}
Within the ZStack
directly below the MeshGradient
ForEach(dragPoints.indices, id: \.self) { index in
Circle()
.fill(Color.white)
.frame(width: 10, height: 10)
.padding(5)
.shadow(color: .black, radius: 0.5, x: 0.5, y: 0.5)
.position(
x: dragPoints[index].x * geometry.size.width,
y: dragPoints[index].y * geometry.size.height
)
}
As you see, not all dots are visible – so let’s move them a little bit inside the screen, just a little bit i.e. in the definition of dragPoints
move all dots just bei 0.01
:
@State private var dragPoints: [CGPoint] = [
CGPoint(x: 0.01, y: 0.01), CGPoint(x: 0.5, y: 0.01), CGPoint(x: 0.99, y: 0.01),
CGPoint(x: 0.01, y: 0.5), CGPoint(x: 0.5, y: 0.5), CGPoint(x: 0.99, y: 0.5),
CGPoint(x: 0.01, y: 0.99), CGPoint(x: 0.5, y: 0.99), CGPoint(x: 0.99, y: 0.99)
]
(Let’s ignore the white frame for a moment.)
We can make those dragPoints
draggable by attaching DragGesture
to each of our dragPoints
:
ForEach(dragPoints.indices, id: \.self) { index in
Circle()
.fill(Color.white)
.frame(width: 10, height: 10)
.padding(5)
.shadow(color: .black, radius: 0.5, x: 0.5, y: 0.5)
.position(
x: dragPoints[index].x * geometry.size.width,
y: dragPoints[index].y * geometry.size.height
)
.gesture(
DragGesture()
.onChanged { value in
let newX = value.location.x / geometry.size.width
let newY = value.location.y / geometry.size.height
dragPoints[index] = CGPoint(
x: min(max(newX, 0.01), 0.99),
y: min(max(newY, 0.01), 0.99)
)
}
)
}
We need to scale the x
and y
location values by the width and height of the screen respectively to ensure that the values stay within the [0,1]
range which is required for the MeshGradient
. The min(max(newX, 0.01), 0.99)
and min(max(newY, 0.01), 0.99)
just keeps the dots a little bit more inside the screen as seen before.
Grab the middle point – you can move it around and the shade is changing accordingly! Now, grab a dot at the outside e.g. the middle-row-left dot. If you move it around, you’ll see that now much more white is seen. Think of it like a canvas and you span a linen over it. Moving the dots is like moving the linen which makes the canvas underneath visible. To maintain the color theme of the linen, let’s color the canvas with the same colors – just in the default setting.
Therefore, above the definition of dragPoints
, include:
private let framePoints: [CGPoint] = [
CGPoint(x: 0.0, y: 0.0), CGPoint(x: 0.5, y: 0.0), CGPoint(x: 1.0, y: 0.0),
CGPoint(x: 0.0, y: 0.5), CGPoint(x: 0.5, y: 0.5), CGPoint(x: 1.0, y: 0.5),
CGPoint(x: 0.0, y: 1.0), CGPoint(x: 0.5, y: 1.0), CGPoint(x: 1.0, y: 1.0)
]
And in the ZStack
just above the MeshGradient
with the dragPoints
include:
MeshGradient(
width: 3,
height: 3,
points: framePoints.map { [Float($0.x), Float($0.y)] },
colors: [
topColor, topColor, topColor,
middleColor, middleColor, middleColor,
bottomColor, bottomColor, bottomColor
]
)
.ignoresSafeArea(edges: .all)
We have now 2 layers of mesh gradients. The one with the framePoints
is the bottom layer which becomes visible when we drag our dots around which are on the top layer
Now, drag one of the dots again. And now it looks indeed like some art! When moving one of the outside dots, it creates lines (remember the comparison to the linen where you wrap the linen and given we colored the underlying (remember the canvas), it looks like a pattern you are creating on top of the canvas.):

Let’s also insert a Reset button so that you can start over without moving your dragPoints
back (which also might be a challenge to find which dot belonged to which grid point 🙂 )
First, let’s define the initial positions of the draggable points:
@State private var initialDragPoints: [CGPoint] = [
CGPoint(x: 0.01, y: 0.01), CGPoint(x: 0.5, y: 0.01), CGPoint(x: 0.99, y: 0.01),
CGPoint(x: 0.01, y: 0.5), CGPoint(x: 0.5, y: 0.5), CGPoint(x: 0.99, y: 0.5),
CGPoint(x: 0.01, y: 0.99), CGPoint(x: 0.5, y: 0.99), CGPoint(x: 0.99, y: 0.99)
]
Find the closing }
of GeometryReader
and add
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(action: {
dragPoints = initialDragPoints
}, label: {
Image(systemName: "arrow.counterclockwise.circle")
.resizable()
.frame(width: 25, height: 25)
.padding(8)
.shadow(color: .black, radius: 1, x: 1, y: 3)
.foregroundColor(.white)
})
}
}
Great! You can now move around the dots and can reset by pressing this Reset button.
Let’s now make the colors of the mesh gradient changeable. We need to create 3 color pickers – for the top, middle and bottom color. We want to show the color pickers only if the user wants to change the colors. Therefore let’s define a bool variable that holds if the pickers are being shown or not:
@State private var showColorPickers: Bool = true
We will change later the value to false
but to see how it looks, we’ll set it first to true
.
Below .toolbar { ... }
add
.overlay(
VStack {
ColorPicker("", selection: $topColor)
.labelsHidden()
Spacer()
ColorPicker("", selection: $middleColor)
.labelsHidden()
Spacer()
ColorPicker("", selection: $bottomColor)
.labelsHidden()
}
.padding()
.background(.ultraThinMaterial)
.ignoresSafeArea(edges: .bottom)
.offset(x: showColorPickers ? 0 : 150)
.animation(.easeInOut, value: showColorPickers)
, alignment: .trailing
)
With the .offset
modifier we make the color picker visible or move it outside of the screen and by using a easeInOut
animation, the color picker appears sliding in or out.
Great – picking a new color is working! Our mesh gradient is also updated accordingly. Let’s know add a button that shows/ hides the color picker. First, change the value of showPickers
to false
so that it’s hidden by default:
@State private var showColorPickers: Bool = false
We want to add now a second button next to the Reset button that show/hides the color picker. Therefore we need to embed the Reset button in a HStack
: Within the curly brackets of ToolbarItem
add a HStack
and a new Button for the color picker:
HStack {
Button(action: {
withAnimation(.easeInOut) {
showColorPickers.toggle()
}
}, label: {
Image(systemName: showColorPickers ? "xmark.circle" : "paintpalette")
.symbolRenderingMode(showColorPickers ? .monochrome : .multicolor)
.resizable()
.frame(width: 25, height: 25)
.padding(8)
.shadow(color: .black, radius: 1, x: 1, y: 3)
.foregroundColor(.white)
})
// Here comes the code for the Reset button as previously defined
}
You can play around with it in your preview – you can drag the dots and you can change the color of the mesh gradient to create something like this:

Step 6: Integrate all views into a NavigationStack
We are almost done – we want to make all 3 views available in our app and this time we want to use a navigation view to present them. In ContentView
replace the VStack
and all the code inside the VStack
with this:
NavigationStack {
List {
NavigationLink("StaticMeshGradientView", destination: StaticMeshGradientView())
NavigationLink("PrideView", destination: PrideView())
NavigationLink("DraggableMeshView", destination: DraggableMeshView())
}
.navigationTitle("MeshArt")
}
In our DraggableMeshView
, you might observe, that it is almost impossible to drag the grid points at the very left. By swiping right, you will go back to the navigation stack view. This is the default behavior of a navigation stack. The navigation controller installs this gesture recognizer on its view and uses it to pop the topmost view controller off the navigation stack. The gesture recognizer for this is called interactivePopGestureRecognizer
.
To disable this, please create a new Swift
file called DisablePopGesture
and insert the following:
import SwiftUI
struct DisablePopGesture: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let controller = UIViewController()
// Disable the interactive pop gesture once the view appears.
DispatchQueue.main.async {controller.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
}
extension View {
func disableInteractivePopGesture() -> some View {
self.background(DisablePopGesture())
}
}
where
controller.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
disables the interactive pop gesture.
Lastly apply this modifier directly after .toolbar { ... }
in DraggableMeshView
:
.disableInteractivePopGesture()
Now you can drag all grid points without swiping back!
Step 7: Run your app
You see already a preview of your app on the right-hand side in the canvas. To run your app in the simulator, please select a device at the very top-middle of the XCode window – you can also add your own physical device – and press the Play button or simply use Cmd + R
.
Congratulations!
You’ve successfully build an app to be creative with some Mesh Art! 🎉
What you have learned
In this code-along, you’ve learned how to
- Create a static mesh gradient by defining a 3×3 grid with specific points and associated colors.
- Define a stripe mesh gradient and applying it as a text style and how to make it reusable as an extension to
View
. - Enable interactivity by making grid points draggable with
DragGesture
to dynamically alter the gradient. - Add a menu buttons e.g. a reset button to restore initial grid positions and integrated color pickers for real-time color changes.
- Integrate all views into a
NavigationStack
and disabled the interactive pop gesture to improve user experience during dragging.
That’s a lot to cover in just one code-along! You made it this far, very well done! 🎉
That’s a wrap! In the next code-along you will create a very cool ripple effect!
Keep learning, keep building, and let your curiosity guide you. Happy coding! ✨
“The only way to do great work is to love what you do. If you haven’t found it yet, keep looking. Don’t settle.” — Steve Jobs
Download the full project on GitHub: https://github.com/swiftandcurious/MeshArt.git