Part 4 Changing the knob position according to its value | Create the Color Picker with SwiftUI

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.

TOC

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.

Execution result of live preview
Execution result of live preview

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.

Create the Color Picker with SwiftUI

Let's share this post !

Author of this article

Akira Hayashiのアバター Akira Hayashi Representative, Software Engineer

I am an application developer loves programming. This blog is a tech blog, its articles are learning notes. In my work, I mainly focus on desktop and mobile application development, but I also write technical books and teach seminars. The websites of my work and books are here -> RK Kaihatsu.

TOC