Part 2 Displaying the Current Value | Create the Color Picker with SwiftUI

This is a series of articles about creating a color picker in SwiftUI. The current value of the color picker we will create in this series of articles will be displayed as follows.

  • Display labels and values above the gradient view
  • Values can be edit from the keyboard
  • Display a cursor indicating the current value on the gradient view

Add a display of the current value for each channel. We want to display the current value as a string and also allow the user to enter a value from the keyboard, so we will use a text field. In this case, we will create a label for this text field and the channel name.

TOC

Implement a model

The current value is a floating point number from 0.0 to 1.0. Implement a model class with properties to contain the values of each channel; since it will be used in combination with SwiftUI, it should be a conforming class for the ObservableObject protocol.

import SwiftUI

class ColorPickerChannelValue : ObservableObject {
    @Published var red: Double = 0.0
    @Published var green: Double = 0.0
    @Published var blue: Double = 0.0
}

Add a value to the ColorPicker view

The main source of data, the value of each channel, is in the ColorPicker view. Therefore, the ColorPickerChannelValue should be generated and destroyed by the ColorPicker.

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()

// 省略

Create bindings for text fields

TextField can not only display values, but also edit them, so we pass a Binding<String>. At first glance, it seems like you could just pass $ColorPickerChannelValue.red, etc., but since ColorPickerChannelValue is a Double, $ColorPickerChannelValue.red would be Binding<Double>.

So, create the property to be Binding<String> as follows.

import SwiftUI

class ColorPickerChannelValue : ObservableObject {
    @Published var red: Double = 0.0
    @Published var green: Double = 0.0
    @Published var blue: Double = 0.0
    
    var redDisplayString: Binding<String> { Binding(
        get: { String(format: "%g", self.red) },
        set: { if let value = Double($0) {
            self.red = value
        }}
    )}

    var greenDisplayString: Binding<String> { Binding(
        get: { String(format: "%g", self.green) },
        set: { if let value = Double($0) {
            self.green = value
        }}
    )}

    var blueDisplayString: Binding<String> { Binding(
        get: { String(format: "%g", self.blue) },
        set: { if let value = Double($0) {
            self.blue = value
        }}
    )}
}

Create bindings with computed properties

When the type of value to be managed and the type of binding do not match, as in this case, it may be easier to create a Computed Property for the binding. When creating bindings with Computed Property, you can use an initializer that allows you to write a closure for the process of setting and retrieving values, as shown below.

init(get: @escaping () -> Value, set: @escaping (Value) -> Void)

Allow decimal places to be entered (added September 19, 2022)

The implemented Computed Property is fine as a string to be displayed on the label, but when I try to edit it in a text field, I encounter a bug that prevents me from entering decimal places.

The cause is that when it cannot be converted to Double, it reverts to the string in its pre-edit state (the display does not revert, but internally), so for example, if you try to enter 0.1 in the field for the red channel, the behavior is as follows.

  1. When you enter up to 0., 0 is assigned to red. (This is OK)
  2. get of redDisplayString property is called and the string 0.0 is returned. (This is NG)
  3. The field string would be 0.0.

To be able to enter decimal places as well, the string 0. must be able to be maintained.

To do so, change the implementation of redDisplayString, greenDisplayString, and blueDisplayString so that the process is as follows.

  1. Create properties that holds the strings being input as a string.
  2. The getter of property returns a property of 1.
  3. The property setter updates the value only when it can be converted to Double. At the same time, a string is assigned to the property 1, regardless of whether it can be converted or not.

The code would look something like this.

import SwiftUI

class ColorPickerChannelValue : ObservableObject {
    @Published var red: Double = 0.0
    @Published var green: Double = 0.0
    @Published var blue: Double = 0.0
    
    private var redEnteredString: String?
    private var greenEnteredString: String?
    private var blueEnteredString: String?
    
    var redDisplayString: Binding<String> { Binding(
        get: {
            self.redEnteredString ?? String(format: "%g", self.red)
        },
        set: {
            self.redEnteredString = $0
            if let value = Double($0) {
                self.red = value
            }
        }
    )}

    var greenDisplayString: Binding<String> { Binding(
        get: {
            self.greenEnteredString ?? String(format: "%g", self.green)
        },
        set: {
            self.greenEnteredString = $0
            if let value = Double($0) {
                self.green = value
            }
        }
    )}

    var blueDisplayString: Binding<String> { Binding(
        get: {
            self.blueEnteredString ?? String(format: "%g", self.blue)
        },
        set: {
            self.blueEnteredString = $0
            if let value = Double($0) {
                self.blue = value
            }
        }
    )}
}

Create labels and text fields

Create labels and text fields. Arrange the following two horizontally for each channel.

  • Label
  • Text field

The code would look like this.

HStack {
    Text("Red: ")
        .font(.title)
    TextField("", text: channelValue.redDisplayString)
        .textFieldStyle(.roundedBorder)
}

Create other channels too, the code would look like this.

var body: some View {
    VStack {
        HStack {
            Text("Red: ")
                .font(.title)
            TextField("", text: channelValue.redDisplayString)
                .textFieldStyle(.roundedBorder)
        }
        PickerGradationView(startColor: redStartColor, endColor: redEndColor)
            .frame(height: 100)
            .padding()
        
        HStack {
            Text("Green: ")
                .font(.title)
            TextField("", text: channelValue.greenDisplayString)
                .textFieldStyle(.roundedBorder)
        }
        PickerGradationView(startColor: greenStartColor, endColor: greenEndColor)
            .frame(height: 100)
            .padding()

        HStack {
            Text("Blue: ")
                .font(.title)
            TextField("", text: channelValue.blueDisplayString)
                .textFieldStyle(.roundedBorder)
        }
        PickerGradationView(startColor: blueStartColor, endColor: blueEndColor)
            .frame(height: 100)
            .padding()
    }
}

If you look at the preview in this state, you will see the following, which looks unbalanced.

Preview 1
Preview 1

Adjust balance

Adjust the balance. Add margins to the left edge of the label and the right edge of the text field, as there are no margins. It seems better to add padding to the VStack, which contains these items, than to put it in each label. Then, the padding in the PickerGradationView would be superfluous, so we would remove these.

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()
            
    var body: some View {
        VStack {
            HStack {
                Text("Red: ")
                    .font(.title)
                TextField("", text: channelValue.redDisplayString)
                    .textFieldStyle(.roundedBorder)
            }
            PickerGradationView(startColor: redStartColor, endColor: redEndColor)
                .frame(height: 100)
            
            HStack {
                Text("Green: ")
                    .font(.title)
                TextField("", text: channelValue.greenDisplayString)
                    .textFieldStyle(.roundedBorder)
            }
            PickerGradationView(startColor: greenStartColor, endColor: greenEndColor)
                .frame(height: 100)

            HStack {
                Text("Blue: ")
                    .font(.title)
                TextField("", text: channelValue.blueDisplayString)
                    .textFieldStyle(.roundedBorder)
            }
            PickerGradationView(startColor: blueStartColor, endColor: blueEndColor)
                .frame(height: 100)
        }
        .padding()
    }
}
Preview 2
Preview 2

Before creating the labels, I had the impression that removing the padding on the PickerGradationView would result in too much space between the top and bottom, but the labels and text field made it just right.

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