Generic Rectangle View in Gradient and Chroma Game

Gradient Game and Chroma Game are going under a major overhaul, both code and design-wise.

I want to share most of the screens between the two and the logic as well. You can find RRComponentsKit for the views, and RRColorKit for the logic.

Each screen (RGB, HSB, CMYK) consist of two rounded rectangles, where the above one represents the target color/gradient while the second is of the user. Do download and play the games to understand what I’m going to talk about next.

Every screen on Gradient Game had its own implementation of this view, while Chroma Game had its own. I don’t know why I didn’t create a generic view in the first place.

For the next update, I created a generic BoxView that takes a heading, a fill for the background, and a view to represent the score.

Heading

The heading is relatively more straightforward. I just had to accept the heading as a parameter and pass it to the Text view. As there are only two types of header- target and yours, I made a BoxHeaderType enum.

public enum BoxHeaderType: String {
    case target
    case yours
}

I made some customisation to show a translucent background in iOS 14. This will replace by the ultraThinMaterial in iOS 15.

Text(header.rawValue.uppercased())
    .foregroundColor(.white)
    .kerning(1.0)
    .font(type: .montserrat, weight: .regular, style: .caption1)
    .padding(8)
    .background(RoundedRectangle(cornerRadius: Constants.cornerRadius / 2)
                    .foregroundColor(Color.black.opacity(0.2)))
    .padding(8)
    .accessibility(addTraits: .isHeader)

Fill

Both Gradient Game and Chroma Game have different requirements for coloring the rounded rectangle. We fill the shape with a color or gradient. If you see the documentation for the fill modifier, it requires the content to conform to ShapeStyle.

@inlinable public func fill<S>(_ content: S, style: FillStyle = FillStyle()) -> some View where S : ShapeStyle

As both Color and LinearGradient conform to ShapeStyle, the fill modifier accepts a view that conforms to the ShapeStyle.

RoundedRectangle(cornerRadius: Constants.cornerRadius)
    .fill(fill)
    .overlay(RoundedRectangle(cornerRadius: Constants.cornerRadius)
                .stroke(Color.primary.opacity(0.1)))

Content

After evaluating the gradient/color, the color model values are shown on this box view itself. However, this cannot be generalised as each type has different requirements. Chroma Game just needs to show one set of values, while Gradient Game displays two values, one for each color of the gradient. Also, RGB and CMYK cannot use the same view.

So, I just accept a content view that conforms to the View protocol, and the main view decides its content.

BoxView

Putting together everything discussed, I came up with the final solution to my initial problem -

public struct BoxView<Content: View, Fill: ShapeStyle>: View {
    let header: BoxHeaderType
    let fill: Fill
    let content: Content

    public init(_ header: BoxHeaderType, _ fill: Fill, @ViewBuilder content: () -> Content) {
        self.header = header
        self.fill = fill
        self.content = content()
    }

    public var body: some View {
        ZStack(alignment: .top) {
            RoundedRectangle(cornerRadius: Constants.cornerRadius)
                .fill(fill)
                .overlay(RoundedRectangle(cornerRadius: Constants.cornerRadius)
                            .stroke(Color.primary.opacity(0.1)))

            VStack {
                Text(header.rawValue.uppercased())
                    .foregroundColor(.white)
                    .kerning(1.0)
                    .font(type: .montserrat, weight: .regular, style: .caption1)
                    .padding(8)
                    .background(RoundedRectangle(cornerRadius: Constants.cornerRadius / 2)
                                    .foregroundColor(Color.black.opacity(0.2)))
                    .padding(8)
                    .accessibility(addTraits: .isHeader)

                content
            }
        }
    }
}

Usage

In Chroma Game, I use it in CMYKView -

BoxView(.target, viewModel.targetColor.new()) {
    // Here goes the result view implementation
}

BoxView(.yours, viewModel.userColor.new()) {
    // Here goes the result view implementation
}

In GradientGame, I use it in RGBView -

BoxView(.target, gradient(with: viewModel.targetGradient.new())) {
    if viewModel.isResultScreenPresented {
        RGBResultsView(gradient: viewModel.targetGradient)
    }
}

BoxView(.yours, gradient(with: viewModel.userGradient.new())) {
    if viewModel.isResultScreenPresented {
        RGBResultsView(gradient: viewModel.userGradient)
    }
}

private func gradient(with gradient: Gradient) -> LinearGradient {
    LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing)
}

With this implementation, I can easily add more variants in the future like RadialGradient, EllipticalGradient and AngularGradient as these conform to the ShapeStyle, and I’m isolating the BoxView from accepting a particular type of gradient.

I hope to document more about my design and code implementations. Thank you for reading! If you’ve any suggestions to improve my code, I would love to hear your thoughts!