This is a series of articles on creating a color picker in SwiftUI. In the previous article, we have already implemented the placement of the knob, but it is fixed at the location where the value is 0
, regardless of the current value. In this article, we will implement a process to change the display position depending on the value.
Calculate display position
In the color picker implemented in this series, the current value of 0.0 should be displayed on the left edge and 1.0 on the right edge; the Y coordinate should be center-aligned. To implement this specification, the center coordinates of the knob are calculated using the following formula.
X = View Width * Current Value
Y = View Height / 2
Get the size of the PickerGradationView
Implement the process of getting the size of the PickerGradationView
; see the following article on how to get the size of any view in SwiftUI.

Add a property to put the size obtained
Add a property to the ColorPicker
that assigns the size of the PickerGradationView
. Add code to ColorPicker.swift
as follows.
import SwiftUI
struct ColorPicker: View {
// omission ...
@StateObject var channelValue = ColorPickerChannelValue()
@State var redGradationViewSize = CGSize()
@State var greenGradationViewSize = CGSize()
@State var blueGradationViewSize = CGSize()
Get the sizes of the three PickerGradationViews
Implement a process to dynamically get the size of the PickerGradationView
and assign it to the added property. As described in the related article, place a GeometryReader
in the background with the background
modifier and check its size.
Assigning the retrieved size to a property causes the view to be rebuilt by SwiftUI. Since it is not possible to start another rebuild while a rebuild is in progress, we use DispatchQueue
to delay the process. To minimize the number of rebuilds, we also check the value and assign it only if it has changed.
PickerGradationView(startColor: redStartColor, endColor: redEndColor)
.frame(height: 100)
.background {
GeometryReader { geometry in
Path { path in
DispatchQueue.main.async {
if geometry.size != redGradationViewSize {
redGradationViewSize = geometry.size
}
}
}
}
}
This code is for redGradationViewSize
. Similar code is implemented for greenGradationViewSize
and blueGradationViewSize
. See “Entire Code After Implementation” at the bottom of this page for the entire code after implementation.
Move the knob with the current value
Implement the process of moving the knob by the current value. The knob is moved by changing the coordinates passed to the offset
modifier in the PickerIndicator
view.
Implement the process of calculating X-coordinates
Add a computed property that calculates the X coordinate of the knob for each channel. Add code as follows.
import SwiftUI
struct ColorPicker: View {
// omission
var redIndicatorX: CGFloat {
redGradationViewSize.width * channelValue.red - 10
}
var greenIndicatorX: CGFloat {
greenGradationViewSize.width * channelValue.green - 10
}
var blueIndicatorX: CGFloat {
blueGradationViewSize.width * channelValue.blue - 10
}
As I wrote in Part 3, I need to shift it by half the width of the PickerIndicator
view, so I return a value of -10
.

Pass to PickerIndicator.offset
Change the value passed to the offset
modifier in the PickerIndicator
view to the computing property you added. The code will look something like this.
PickerIndicator()
.frame(width: 20, height: 20)
.offset(x: redIndicatorX)
This code is for the Red channel. Similar changes are made to the PickerIndicator
view for the Green and Blue channels.
Entire Code After Implementation
The entire code after implementation looks like this.
import SwiftUI
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()
@State var redGradationViewSize = CGSize()
@State var greenGradationViewSize = CGSize()
@State var blueGradationViewSize = CGSize()
var redIndicatorX: CGFloat {
redGradationViewSize.width * channelValue.red - 10
}
var greenIndicatorX: CGFloat {
greenGradationViewSize.width * channelValue.green - 10
}
var blueIndicatorX: CGFloat {
blueGradationViewSize.width * channelValue.blue - 10
}
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)
.background {
GeometryReader { geometry in
Path { path in
DispatchQueue.main.async {
if geometry.size != redGradationViewSize {
redGradationViewSize = geometry.size
}
}
}
}
}
PickerIndicator()
.frame(width: 20, height: 20)
.offset(x: redIndicatorX)
}
HStack {
Text("Green: ")
.font(.title)
TextField("", text: channelValue.greenDisplayString)
.textFieldStyle(.roundedBorder)
}
ZStack(alignment: .leading) {
PickerGradationView(startColor: greenStartColor, endColor: greenEndColor)
.frame(height: 100)
.background {
GeometryReader { geometry in
Path { path in
DispatchQueue.main.async {
if geometry.size != greenGradationViewSize {
greenGradationViewSize = geometry.size
}
}
}
}
}
PickerIndicator()
.frame(width: 20, height: 20)
.offset(x: greenIndicatorX)
}
HStack {
Text("Blue: ")
.font(.title)
TextField("", text: channelValue.blueDisplayString)
.textFieldStyle(.roundedBorder)
}
ZStack(alignment: .leading) {
PickerGradationView(startColor: blueStartColor, endColor: blueEndColor)
.frame(height: 100)
.background {
GeometryReader { geometry in
Path { path in
DispatchQueue.main.async {
if geometry.size != blueGradationViewSize {
blueGradationViewSize = geometry.size
}
}
}
}
}
PickerIndicator()
.frame(width: 20, height: 20)
.offset(x: blueIndicatorX)
}
}
.padding()
}
}
Check preview
Check that the previews are displayed as intended: since there are three channels, change the code in ColorPicker_Previews
in ColorPicker.swift
so that the previews for the Red channel = 0.0
, Green channel = 0.5
, and Blue channel = 1.0
are displayed.
struct ColorPicker_Previews: PreviewProvider {
static var channelValue: ColorPickerChannelValue {
let value = ColorPickerChannelValue()
value.red = 0.0
value.green = 0.5
value.blue = 1.0
return value
}
static var previews: some View {
ColorPicker(channelValue: channelValue)
.previewInterfaceOrientation(.portrait)
}
}
The value of each channel is retrieved from the ColorPicker.channelValue
property, so when ColorPicker_Previews
creates a ColorPicker
view, it can be implemented by passing the ColorPickerChannelValue
that stores the value you want to display.
When you run this code, you will see a preview as follows. The knob appears in the expected position.

Serialized Article
This article is part of a series of articles titled “Create the Color Picker with SwiftUI”. Other articles in the series are here.