WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

Day 46, challenge ColorCycling: not sure what is meant, two solutions follow. Comments invited.

Forums > 100 Days of SwiftUI

In the third challenge for the Drawing app, the following is asked:

Create a ColorCyclingRectangle shape that is the rectangular cousin of ColorCyclingCircle, allowing us to control the position of the gradient using one or more properties.

I was not sure what exactly is meant. Do we literally have to use a Gradient? The original solution did not, but worked with insets, all stroked with a different color.

Also, we are asked to use a rectangle, but is this just the surrounding shape or do the different colors also need to have rectangular shape?

In the end, I decided to make two solutions. The user can switch using a toggle.

The first is analogous to the given solution in that it is circular, however embedded in a (rounded) rectangular shape and implemented using a RadialGradient. The center can be moved through sliders for X and Y. This one was not too difficult.

The second was to have (rounded) rectangular shapes everywhere, getting smaller and smaller and so that they "disappear" in a point. This point can also be moved. At first I thought to use inset(by:), but that appears not to be possible in SwiftUI: one cannot have separate values for the four directions. And I need to, in order to be able to move the center. So I had to forego inset(by:), and size and position all rectangles explicitly.

I did consider creating my own inset(by insets: EdgeInsets) modifier, but found it not necessary, and probably more complicated than necessary, once I found out how simple it is to size and position a rectangle.

It cost me quite some time to arrive at this solution and I had a few false starts, but in the end it does not seem too complicated. Any comments or suggestions are welcome.

import SwiftUI

struct ColorsView: View {
    @State private var colorCycle = 0.0
    @State private var locX = 50.0
    @State private var locY = 50.0
    @State private var useGradient = true

    let baseCornerRadius = 25.0

    var body: some View {
        VStack {
            Toggle("Use gradient?", isOn: $useGradient)
            ZStack {
                GeometryReader { geo in
                    let w = geo.size.width
                    let h = geo.size.height
                    let m = min(w,h)
                    let centerX = locX * w / 100
                    let centerY = locY * h / 100
                    VStack {
                        if useGradient {
                            let center = UnitPoint(x: centerX / w, y: centerY / h)
                            RoundedRectangle(cornerRadius: baseCornerRadius)
                                .fill(
                                    RadialGradient(gradient: Gradient(colors: colors(steps: Int(m), amount: colorCycle)), center: center, startRadius: 0, endRadius: m/2))
                        } else {
                            ZStack {
                                ForEach(0..<Int(m), id: \.self) { value in
                                    let v = CGFloat(value)
                                    let cornerRadius = baseCornerRadius * (m - Double(value)) / m
                                    RoundedRectangle(cornerRadius: cornerRadius)
                                        .size(width: w-(v*w/m), height: h-(v*h/m))
                                        .offset(x: v*(centerX/m), y: v*(centerY/m))
                                        .stroke(color(for: value, amount: colorCycle, steps: Int(m)), lineWidth: 2)

                                }
                            }
                        }
                    }
                }
            }

            Text("Color cycle")
            Slider(value: $colorCycle)

            Text("centerX")
            Slider(value: $locX, in: 5...95, step: 1)

            Text("centerY")
            Slider(value: $locY, in: 5...95, step: 1)
        }
    }

    func color(for value: Int, amount: Double, steps: Int) -> Color {
        var targetHue = Double(value) / Double(steps) + amount

        targetHue = targetHue.truncatingRemainder(dividingBy: 1)

        return Color(hue: targetHue, saturation: 1, brightness: 1)
    }

    func colors(steps: Int, amount: Double) -> [Color] {
        var colors = [Color]()

        for i in 0..<steps {
            colors.append(color(for: i, amount: amount, steps: steps))
        }

        return colors
    }
}

   

Hacking with Swift is sponsored by Fernando Olivares

SPONSORED Fernando's book will guide you in fixing bugs in three real, open-source, downloadable apps from the App Store. Learn applied programming fundamentals by refactoring real code from published apps. Hacking with Swift readers get a $10 discount!

Read the book

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.