This is a series of articles on creating a color picker in SwiftUI. In the previous article, we implemented the placement of the knob, but it remains fixed at the location where the value is 0
, regardless of the current value. This article will cover implementing a process to change the display position based 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. The center coordinates of the knob are calculated using the following formula to implement this specification.
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
To add a property to the ColorPicker
that assigns the size of the PickerGradationView
, add the following code to ColorPicker.swift
.
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 get the size of the PickerGradationView
dynamically and assign it to the added property. Then, as the related article describes, 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 impossible 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
. A 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 according to 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 the 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
is 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.