Protocols in Gradient and Chroma Game

Gradient Game is my first app on the App Store. I started working on it in early August 2019, when I wanted to improve upon a SwiftUI tutorial and create something of my own.

While I was really excited about shipping it on the day of the launch of iOS 13, I naively wrote a lot of unnecessary code without understanding SwiftUI and no proper architecture for future releases.

And it started piling up because I wanted to “fix” it later, and later never came.

It’s July 2021, and the first reaction looking at the codebase after months is -

IT IS A MESS.

In March 2020, I wanted to add a significant feature to it - Arcade Mode. And while implementing it, I realized that the code is a mess, and it was an immense pain to achieve what I wanted in a short duration of time without making breaking changes.

So I never pushed that feature to the public. I also wanted to add Game Center capability but abandoned that effort too because of the above reason.

I also released another game called Chroma Game back in February 2020, and it suffered the same fate. I checked yesterday that the last time I released a version was in March 2020. Sigh.

I got a mail yesterday where a user wanted me to add CMYK support in Chroma, and they were pretty excited about it. I had no energy left to look at the garbage I wrote fifteen months ago. But, reading that mail lit my face up enough to dreadfully open Xcode and navigate to that project.

Scanning the Codebase

The whole codebase is just dozens of View.

No Model.

No ViewModel.

Just a lot of massive views.

The first thing is to separate the model from the view. Currently, it looks something like this -

struct RGBView: View {
    @State var targetRed = random()
    @State var targetGreen = random()
    @State var targetBlue = random()

    @State var yourRed = rgbColor
    @State var yourGreen = rgbColor
    @State var yourBlue = rgbColor

    // rest of the thousands of lines of code
}

I prefer a shared framework for both Gradient Game and Chroma Game with a generic approach towards the code.

Protocols

Let’s start with a protocol.

Every color requires only one thing - create a new color, irrespective of RGB, HSB, or CMYK. So I started off with ColorProtocol -

protocol ColorProtocol {
  func new() -> Color
}

Conforming to the ColorProtocol, I created individual protocols for each color model -

protocol RGBColorProtocol: ColorProtocol {
    var red: Double { get set }
    var green: Double { get set }
    var blue: Double { get set }
}

extension RGBColorProtocol {
    func new() -> Color {
        Color(red: red, green: green, blue: blue)
    }
}

protocol HSBColorProtocol: ColorProtocol {
    var hue: Double { get set }
    var saturation: Double { get set }
    var brightness: Double { get set }
}

extension HSBColorProtocol {
    func new() -> Color {
        Color(hue: hue, saturation: saturation, brightness: brightness)
    }
}

protocol CMYKColorProtocol: ColorProtocol {
    var cyan: Double { get set }
    var magenta: Double { get set }
    var yellow: Double { get set }
    var black: Double { get set }
}

extension CMYKColorProtocol {
    func new() -> Color {
        Color(cyan: cyan, magenta: magenta, yellow: yellow, black: black)
    }
}

extension Color {
    init(cyan: Double, magenta: Double, yellow: Double, black: Double) {
        let red = (1 - cyan) * (1 - black)
        let green = (1 - magenta) * (1 - black)
        let blue = (1 - yellow) * (1 - black)

        self.init(red: red, green: green, blue: blue)
    }
}

The GradientProtocol is based on creating a gradient between two colors -

protocol GradientProtocol {
    var startColor: ColorProtocol { get set }
    var endColor: ColorProtocol { get set }

    func new() -> Gradient
}

extension GradientProtocol {
    func new() -> Gradient {
        Gradient(colors: [startColor.new(), endColor.new()])
    }
}

Based on the individual protocols, I created multiple structs for regular use and testing. For example, this is for RGB View -

class RGBRandomColor: RGBColorProtocol {
    var red: Double = Constants.random
    var green: Double = Constants.random
    var blue: Double = Constants.random
}

class RGBUserColor: RGBColorProtocol {
    static let initial: Double = 188/255

    var red: Double = RGBInitialColor.initial
    var green: Double = RGBInitialColor.initial
    var blue: Double = RGBInitialColor.initial
}

class RGBTestStartColor: RGBColorProtocol {
    var red: Double = 19/255
    var green: Double = 84/255
    var blue: Double = 122/255
}

class RGBTestEndColor: RGBColorProtocol {
    var red: Double = 128/255
    var green: Double = 208/255
    var blue: Double = 199/255
}

struct Constants {
    static var random: Double {
        Double.random(in: 0...1)
    }
}

class RGBRandomGradient: GradientProtocol {
    var startColor: ColorProtocol = RGBRandomColor()
    var endColor: ColorProtocol = RGBRandomColor()
}

class RGBUserGradient: GradientProtocol {
    var startColor: ColorProtocol = RGBUserColor()
    var endColor: ColorProtocol = RGBUserColor()
}

class RGBTestGradient: GradientProtocol {
    var startColor: ColorProtocol = RGBTestStartColor()
    var endColor: ColorProtocol = RGBTestEndColor()
}

Now, I created RGBViewModel that contains all the logic related to the RGB game. I’m still trying to figure out how to make it generic across all the colors.

class RGBViewModel: ObservableObject {
    @Published var targetColor: RGBColorProtocol
    @Published var userColor: RGBColorProtocol

    init(targetColor: RGBColorProtocol, userColor: RGBColorProtocol) {
        self.targetColor = targetColor
        self.userColor = userColor
    }
}

In the MainView, I created a @StateObject variable and inject the required classes -

@StateObject private var viewModel = RGBViewModel(targetColor: RGBRandomColor(), userColor: RGBInitialColor())

Conclusion

My initial implementation of stuffing everything into the view has turned into just using the RGBViewModel to display the data.

@EnvironmentObject var viewModel: RGBViewModel

I learned a lot in this process, especially understanding how I unsuccessfully tried to conform to a non-concrete type protocol to make the view model generic. My next aim is to have a base view model and a base view with the standard properties required for the different color models.

If you’ve any suggestions to improve my code, I would love to hear your thoughts!