Part 6 Organizing Code | Create the Color Picker with SwiftUI

This series of articles resolves around creating a color picker in SwiftUI. Initially, I envisioned the code to be on a smaller scale and, thus, added code with minimal organization. However, this approach led to a convoluted codebase. So in this article, I would like to organize the code.

目次

Model for a channel

The color picker we created in this series is an RGB model with 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.

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

With these considerations in mind, we proceed to create model classes.

  • ColorPickerChannelModel
  • ColorPickerGradationModel

Implement the ColorPickerChannelModel class

Let’s now implement the ColorPickerChannelModel class.

The currentValue has a fixed range of possible values. However, 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)
            }
        }

    }
    
    /// Update the current 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. Then, implement it simply with stored properties.

Also, each channel has a different color. Still, 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

Previously, the ColorPicker was responsible for managing both the PickerGradationView and PickerIndicator and the information distributed across different 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. The ColorPicker view created labels for all channels in the previous implementation. This makes the code confusing so we will cut it to a view 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 the 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 was divided into models and views. As a result, 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. After organizing this article, the code is based on MVVM, with the VM (View Model) removed. At this scale, View and Model are 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.

Regardless, having a well-structured and readable code is more important than adhering to a specific architectural pattern.

Download the code

Click here to download the code created for this article.

著書紹介

Authored Books

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

Akira Hayashi (林 晃)のアバター Akira Hayashi (林 晃) Representative(代表), Software Engineer(ソフトウェアエンジニア)

アールケー開発代表。Appleプラットフォーム向けの開発を専門としているソフトウェアエンジニア。ソフトウェアの受託開発、技術書執筆、技術指導・セミナー講師。note, Medium, LinkedIn
-
Representative of RK Kaihatsu. Software Engineer Specializing in Development for the Apple Platform. Specializing in contract software development, technical writing, and serving as a tech workshop lecturer. note, Medium, LinkedIn

目次