Part 3 Displaying the Current Value Knob | Create the Color Picker with SwiftUI

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 at 1.0.
  • The Y coordinate should be centered to the height of the gradient view.
TOC

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.

Drawing a circle in SwiftUI
Drawing a circle in SwiftUI

A white circle with a black border can be implemented with the following process.

  1. Draw a thicker black circle.
  2. 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.

Bordering the circle with SwiftUI
Bordering the circle with SwiftUI

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.

Overlay PickerIndidator on PickerGradationView
Overlay PickerIndidator on PickerGradationView

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.

Execution Result
Execution Result

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.

Preview after offset adjustment
Preview after offset adjustment

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.

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