This is a series of articles on creating a color picker in SwiftUI. In the previous article, we created a text field to display the current value, and in this article, we will continue the process by implementing the look and feel of the process to display a knob indicating the current value on the gradient view.
The shape of the knob, for this article, we would like to use the following shape.
- Black borders on white circles.
- The X coordinate is placed so that the center of the circle is at the position of the current value, with the left end at
0.0
and the right end at1.0
. - The Y coordinate should be centered to the height of the gradient view.
Drawing a circle in SwiftUI
To draw a circle in SwiftUI, use a combination of GeometryReader and Path. For example, the following code.
struct PickerIndicator: View {
var body: some View {
GeometryReader { geometry in
Path { path in
let minLength = min(geometry.size.width, geometry.size.height)
path.addEllipse(in: CGRect(x: 0, y: 0, width: minLength, height: minLength))
}
.stroke(lineWidth: 1)
.foregroundColor(.black)
}
}
}
When this is run, you will see the following.

A white circle with a black border can be implemented with the following process.
- Draw a thicker black circle.
- Draw a white circle with a thin line on top.
In SwiftUI, this can be achieved with the following code.
struct PickerIndicator: View {
var body: some View {
GeometryReader { geometry in
Path { path in
let minLength = min(geometry.size.width, geometry.size.height)
path.addEllipse(in: CGRect(x: 0, y: 0, width: minLength, height: minLength))
}
.stroke(lineWidth: 10)
.foregroundColor(.black)
Path { path in
let minLength = min(geometry.size.width, geometry.size.height)
path.addEllipse(in: CGRect(x: 0, y: 0, width: minLength, height: minLength))
}
.stroke(lineWidth: 3)
.foregroundColor(.white)
}
}
}
When you run this code, you will see the following.

Draw indicator on gradient view
Let’s draw an indicator on the gradient view for each channel. Based on the code in “Drawing a circle in SwiftUI”, we will implement the indicator drawing.
Add a PickerIndicator to the ColorPicker view
Add a PickerIndicator
view and use the code as described in “Drawing Circles in SwiftUI”.
With that in place, add the following code to the ColorPicker
to display a PickerIndicator
in the center of the gradient view for each channel. The code will look something like this.
struct ColorPicker: View {
var redStartColor: Color = Color(red: 0.0, green: 0.0, blue: 0.0)
var redEndColor: Color = Color(red: 1.0, green: 0.0, blue: 0.0)
var greenStartColor: Color = Color(red: 0.0, green: 0.0, blue: 0.0)
var greenEndColor: Color = Color(red: 0.0, green: 1.0, blue: 0.0)
var blueStartColor: Color = Color(red: 0.0, green: 0.0, blue: 0.0)
var blueEndColor: Color = Color(red: 0.0, green: 0.0, blue: 1.0)
@StateObject var channelValue = ColorPickerChannelValue()
var body: some View {
VStack {
HStack {
Text("Red: ")
.font(.title)
TextField("", text: channelValue.redDisplayString)
.textFieldStyle(.roundedBorder)
}
ZStack(alignment: .leading) {
PickerGradationView(startColor: redStartColor, endColor: redEndColor)
.frame(height: 100)
PickerIndicator()
.frame(width: 20, height: 20)
}
HStack {
Text("Green: ")
.font(.title)
TextField("", text: channelValue.greenDisplayString)
.textFieldStyle(.roundedBorder)
}
ZStack(alignment: .leading) {
PickerGradationView(startColor: greenStartColor, endColor: greenEndColor)
.frame(height: 100)
PickerIndicator()
.frame(width: 20, height: 20)
}
HStack {
Text("Blue: ")
.font(.title)
TextField("", text: channelValue.blueDisplayString)
.textFieldStyle(.roundedBorder)
}
ZStack(alignment: .leading) {
PickerGradationView(startColor: blueStartColor, endColor: blueEndColor)
.frame(height: 100)
PickerIndicator()
.frame(width: 20, height: 20)
}
}
.padding()
}
}
Since the PickerIndicator
view is overlaid on the PickerGradationView
, I moved the PickerGradationView
into the ZStack
.
The PickerIndicator
view is too large in its default state, so it is resized to just the right size using the frame
modifier.
ZStack
is centered by default. We changed the alignment to left-aligned (leading) by specifying .leading
as the argument alignment
.
When you run this code, you will see the following.

Adjust the zero position of the PickerIndicator
Look closely at the preview and you will see that the PickerIndicator
is slightly off to the right.
Add the code below from “// Draw a bounding box
” to the PickerIndicator
as follows. It draws a bounding box of the GeometryReader
.
struct PickerIndicator: View {
var body: some View {
GeometryReader { geometry in
Path { path in
let minLength = min(geometry.size.width, geometry.size.height)
path.addEllipse(in: CGRect(x: 0, y: 0, width: minLength, height: minLength))
}
.stroke(lineWidth: 10)
.foregroundColor(.black)
Path { path in
let minLength = min(geometry.size.width, geometry.size.height)
path.addEllipse(in: CGRect(x: 0, y: 0, width: minLength, height: minLength))
}
.stroke(lineWidth: 3)
.foregroundColor(.white)
// Draw a bounding box
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: geometry.size.width - 1, y: 0))
path.addLine(to: CGPoint(x: geometry.size.width - 1, y: geometry.size.height - 1))
path.addLine(to: CGPoint(x: 0, y: geometry.size.height - 1))
path.addLine(to: CGPoint(x: 0, y: 0))
}
.stroke(lineWidth: 1)
.foregroundColor(.yellow)
}
}
}
You can see that the left edge of the bounding box is the alignment position by ZStack
.

When the PickerIndicator
has a value of 0
and is on the left edge, the center of the PickerIndicator
must be on the left edge of the PickerGradationView
. To do so, the PickerIndicator
must be shifted to the left by half of its width.
The offset
modifier can be used to adjust the position. Add the offset
modifier to PickerIndicator()
as follows.
ZStack(alignment: .leading) {
PickerGradationView(startColor: redStartColor, endColor: redEndColor)
.frame(height: 100)
PickerIndicator()
.frame(width: 20, height: 20)
.offset(x: -10)
}
When you run this code, you will see the following and it will be displayed in the correct position. Also, please remove the bounding box drawing code that you added to the PickerIndicator
, as it is no longer needed.

Serialized Articles
This article is one of a series of articles entitled “Create the Color Picker with SwiftUI”. Please open the next page for other articles in the series.