Part 6 Organizing Code | Create the Color Picker with SwiftUI

This is a series of articles about creating a color picker in SwiftUI. I thought the code would be on a smaller scale, so I came here adding code without much organization, and the code became a mess. In this article, I would like to organize the code.

TOC

Model for a channel

The color picker we are creating in this series is an RGB color model and has the following three channels.

  • Red
  • Green
  • Blue

The information that each channel has can be organized as follows.

  • Channel name
  • Current value
  • The text of the current value field

The following information is also needed to handle the drawing and tapping of gradient views for each channel.

  • View size of the gradient view
  • Start color of the gradient (Left edge color)
  • End color of the gradient (Right edge color)

Based on this, we create model classes.

  • ColorPickerChannelModel
  • ColorPickerGradationModel

Implement the ColorPickerChannelModel class

Implement the ColorPickerChannelModel class.

The currentValue has a fixed range of possible values. If a value is about to be set outside the range, we want to round it to the range, so we have implemented the following.

  • In addition to the property, implement the changeCurrentValue method to check for out-of-range and rounding.
  • Use the changeCurrentValue method for out-of-range checking.

The implemented code is as follows.

import SwiftUI

class ColorPickerChannelModel : ObservableObject {
    /// Channel name
    @Published var channelName: String
    
    /// Current value
    @Published var currentValue: Double = 0.0
    
    init(channelName: String) {
        self.channelName = channelName
    }
    
    /// Text being entered
    private var fieldTextStore: String?
    
    /// Binding of the text being entered
    var fieldText: Binding<String> {
        Binding {
            self.fieldTextStore ?? String(format: "%.1f", self.currentValue)
        } set: {
            self.fieldTextStore = $0
            if let value = Double($0) {
                self.changeCurrentValue(value)
            }
        }

    }
    
    /// 現在値の更新
    func changeCurrentValue(_ value: Double) {
        if value < 0.0 {
            currentValue = 0.0
        } else if value > 1.0 {
            currentValue = 1.0
        } else {
            currentValue = value
        }
    }
    
    /// Clear text being entered
    func clearFieldText() {
        fieldTextStore = nil
    }
}

Implement the ColorPickerGradationModel class

Make the model class capable of holding view size and gradient color by properties. Implement it simply with stored properties.

Also, each channel has a different color, but we want to make it easy to use, so we create a static property that retrieves an instance with information on the Red, Green, and Blue channels.

The implemented code is as follows.

import SwiftUI

class ColorPickerGradationModel : ObservableObject {
    /// Size of the gradient view
    @Published var viewSize: CGSize = CGSize()
    /// Start color of the gradient
    @Published var startColor: Color
    /// End color of the gradient
    @Published var endColor: Color
    
    /// Initializer
    /// - Parameters:
    ///     - startColor: The start color of the gradient
    ///     - endColor: The end color of the gradient
    init(startColor: Color, endColor: Color) {
        self.startColor = startColor
        self.endColor = endColor
    }
}

extension ColorPickerGradationModel {
    
    /// Red channel of the RGB color model
    static var red: ColorPickerGradationModel {
        ColorPickerGradationModel(startColor: Color(red: 0.0, green: 0.0, blue: 0.0),
                                  endColor: Color(red: 1.0, green: 0.0, blue: 0.0))
    }
    
    /// Green channel of the RGB color model
    static var green: ColorPickerGradationModel {
        ColorPickerGradationModel(startColor: Color(red: 0.0, green: 0.0, blue: 0.0),
                                  endColor: Color(red: 0.0, green: 1.0, blue: 0.0))
    }
    
    /// Blue channel of the RGB color model
    static var blue: ColorPickerGradationModel {
        ColorPickerGradationModel(startColor: Color(red: 0.0, green: 0.0, blue: 0.0),
                                  endColor: Color(red: 0.0, green: 0.0, blue: 1.0))
    }
}

The gradient view

The previous state of the structure was that the ColorPicker managed the PickerGradationView and PickerIndicator, and also managed the information separated into models. This will be changed so that the PickerGradationView has and manages the PickerIndicator.

The model should be passed from the higher level.

The height of the view is also fixed in the PickerGradationView.

Implement the PickerGradationView

The implemented code is as follows.

import SwiftUI

struct PickerGradationView: View {
    @ObservedObject var channel: ColorPickerChannelModel
    @ObservedObject var gradation: ColorPickerGradationModel
    
    /// Size of the knob
    let indicatorSize: CGFloat = 20
    /// Height of the gradient view
    let gradationHeigh: CGFloat = 100
    
    /// X coordinates of the knob
    var indicatorX: CGFloat {
        gradation.viewSize.width * channel.currentValue - indicatorSize / 2
    }
    
    var body: some View {
        ZStack(alignment: .leading) {
            GeometryReader() { geometry in
                LinearGradient(gradient: Gradient(colors: [gradation.startColor, gradation.endColor]), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 0))
            }
            .frame(height: gradationHeigh)
            .background {
                GeometryReader { geometry in
                    Path { path in
                        Task.detached {
                            await updateViewSize(geometry.size)
                        }
                    }
                }
            }
            .onTapGesture { point in
                channel.currentValue = point.x / gradation.viewSize.width
                channel.clearFieldText()
            }
            
            PickerIndicator()
                .frame(width: indicatorSize, height: indicatorSize)
                .offset(x: indicatorX)
        }
    }
    
    @MainActor
    func updateViewSize(_ size: CGSize) async {
        if size != gradation.viewSize {
            gradation.viewSize = size
        }
    }
}

struct PickerGradationView_Previews: PreviewProvider {
    static var previews: some View {
        PickerGradationView(channel: ColorPickerChannelModel(channelName: "Red"), gradation: .red)
            .padding()
    }
}


Preview of the PickerGradationView
Preview of the PickerGradationView

Views for each channel

The color picker in the RGB color model has different values and display colors for each channel, but the structure of the view is the same. In the previous implementation, the ColorPicker view directly created labels, etc. for all channels. This makes the code confusing, so we will cut it out to a view that is created for each channel.

This structure also presented problems and had to be changed later in the series.

Implement the ColorPickerSubView

Create the following views for each channel.

  • PickerGradationView
  • Label for channel name
  • Text field for editing current value

The code would look something like this.

import SwiftUI

struct ColorPickerSubView: View {
    @ObservedObject var channel: ColorPickerChannelModel
    @ObservedObject var gradation: ColorPickerGradationModel
    
    var body: some View {
        VStack {
            HStack {
                Text("\(channel.channelName): ")
                    .font(.title)
                TextField("", text: channel.fieldText)
                    .textFieldStyle(.roundedBorder)
            }
            PickerGradationView(channel: channel, gradation: gradation)
        }
    }
}

struct ColorPickerSubView_Previews: PreviewProvider {
    static var previews: some View {
        ColorPickerSubView(channel: ColorPickerChannelModel(channelName: "Red"), gradation: .red)
            .padding()
    }
}
Preview of the ColorPickerSubView
Preview of the ColorPickerSubView

Color Picker for the RGB color model

We have implemented the views and the necessary models for each channel. The view and model implemented so far are combined to create a color picker for an RGB color model with three channels.

The previously implemented ColorPickerChannelValue.swift file is no longer needed and will be deleted.

The code for ColorPicker.swift will also change significantly; it will be implemented using PickerGradationView, ColorPickerChannelModel, and ColorPickerGradationModel.

The code now looks like this.

import SwiftUI

struct ColorPicker: View {
    @StateObject var redChannel: ColorPickerChannelModel = ColorPickerChannelModel(channelName: "Red")
    @StateObject var redGradation: ColorPickerGradationModel = .red
    
    @StateObject var greenChannel: ColorPickerChannelModel = ColorPickerChannelModel(channelName: "Green")
    @StateObject var greenGradation: ColorPickerGradationModel = .green
    
    @StateObject var blueChannel: ColorPickerChannelModel = ColorPickerChannelModel(channelName: "Blue")
    @StateObject var blueGradation: ColorPickerGradationModel = .blue
    
    var body: some View {
        VStack {
            ColorPickerSubView(channel: redChannel, gradation: redGradation)
            ColorPickerSubView(channel: greenChannel, gradation: greenGradation)
            ColorPickerSubView(channel: blueChannel, gradation: blueGradation)
        }
        .padding()
    }
}

struct ColorPicker_Previews: PreviewProvider {
    static var previews: some View {
        ColorPicker()
            .previewInterfaceOrientation(.portrait)
    }
}
Preview of the ColorPicker
Preview of the ColorPicker

Conclusion

The code had a lot of nests with very poor visibility because everything was pushed into the ColorPicker, but after this reorganization, the code has been separated into models and views, and the nests have become shallower, making the code easier to read.

What is the best architecture to adopt for SwiftUI is often a topic of discussion on the Internet, and MVVM or TCA is often discussed for SwiftUI. The code after organizing this article is based on MVVM, with the VM (View Model) removed. At this scale, I thought just View and Model would be sufficient.

However, some say that ObservableObject is the View Model itself, and if you look at it that way, you could say that it has become MVVM.

In any case, I personally think it is better if the code has a good outlook rather than which architecture it is.

Download the code

Click here to download the code created for this article.

Serialized Articles

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