Exploring MusicKit and Apple Music API
Unlock the full power of MusicKit & Apple Music APIs in your apps with the best guide! Use code musickit-blog for a limited-time 35% discount!
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! Message @rudrankriyam
Exploring MusicKit and Apple Music API
Unlock the full power of MusicKit & Apple Music APIs in your apps with the best guide! Use code musickit-blog for a limited-time 35% discount!