100 Days of SwiftUI Day 46

100 Days of SwiftUI – Day 46

We’ve arrived at day 46 of the 100 Days of SwiftUI! Over the past three days, we learned about various drawing functions in SwiftUI. Today, we’re reviewing what we’ve learned, as well as complete three challenges. As always, I’ll be covering the challenges here. Let’s take a look!

SwiftUI drawing challenge #1

The first challenge is to create an Arrow shape – having it point straight up is fine. The basic version of our arrow is created like this:

//
//  ContentView.swift
//  DrawingChallengeDay46
//


import SwiftUI

struct Arrow: Shape {
    func path(in rect: CGRect) -> Path {
        return Path { path in
            path.move(to: CGPoint(x: rect.midX, y: 0))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
            path.addLine(to: CGPoint(x: rect.midX + 50, y: rect.midY))
            path.addLine(to: CGPoint(x: rect.midX + 50, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.midX - 50, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.midX - 50, y: rect.midY))
            path.addLine(to: CGPoint(x: 0, y: rect.midY))
            path.closeSubpath()
        }
    }
}

struct ContentView: View {
    var body: some View {
        Arrow()
            .fill(.blue)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

As you can see, when creating our path, we’re using a few fixed numbers to position the paths correctly. This can be improved a bit by calculating the arrow height and shaft width in a variable.

struct Arrow: Shape {
    var headHeight = 0.5
    var shaftWidth = 0.5

    func path(in rect: CGRect) -> Path {
        let height = rect.height * headHeight // available height multiplied by how high we want the head to be.
        let thickness = rect.width * shaftWidth / 2
        
        return Path { path in
            path.move(to: CGPoint(x: rect.midX, y: 0))
            path.addLine(to: CGPoint(x: rect.maxX, y: height))
            path.addLine(to: CGPoint(x: rect.midX + thickness, y: height))
            path.addLine(to: CGPoint(x: rect.midX + thickness, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.midX - thickness, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.midX - thickness, y: height))
            path.addLine(to: CGPoint(x: 0, y: height))
            path.closeSubpath()
        }
    }
}

struct ContentView: View {
    var body: some View {
        Arrow()
            .fill(.blue)
    }
}

SwiftUI drawing challenge #2

The second challenge is to make the line thickness of your Arrow shape animatable. In essence, this is quite easy done with animatableData:

//
//  ContentView.swift
//  DrawingChallengeDay46
//


import SwiftUI

struct Arrow: Shape {
    var headHeight = 0.5
    var shaftWidth = 0.5
    
    var animatableData: Double {
        get { shaftWidth }
        set { shaftWidth = newValue}
    }

    func path(in rect: CGRect) -> Path {
        let height = rect.height * headHeight // available height multiplied by how high we want the head to be.
        let thickness = rect.width * shaftWidth / 2
        
        return Path { path in
            path.move(to: CGPoint(x: rect.midX, y: 0))
            path.addLine(to: CGPoint(x: rect.maxX, y: height))
            path.addLine(to: CGPoint(x: rect.midX + thickness, y: height))
            path.addLine(to: CGPoint(x: rect.midX + thickness, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.midX - thickness, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.midX - thickness, y: height))
            path.addLine(to: CGPoint(x: 0, y: height))
            path.closeSubpath()
        }
    }
}

struct ContentView: View {
    @State private var headHeight = 0.5
    @State private var shaftWidth = 0.5
    
    var body: some View {
        Arrow(headHeight: headHeight, shaftWidth: shaftWidth)
            .fill(.blue)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

However, since we have two properties that decide the final design of our arrow, we can also use an animatablePair:

//
//  ContentView.swift
//  DrawingChallengeDay46
//


import SwiftUI

struct Arrow: Shape {
    var headHeight = 0.5
    var shaftWidth = 0.5
    
    var animatableData: AnimatablePair<Double, Double> {
        get { AnimatablePair(headHeight, shaftWidth) }
        set {
            shaftWidth = newValue.first
            headHeight = newValue.second
        }
    }
    
    func path(in rect: CGRect) -> Path {
        let height = rect.height * headHeight // available height multiplied by how high we want the head to be.
        let thickness = rect.width * shaftWidth / 2
        
        return Path { path in
            path.move(to: CGPoint(x: rect.midX, y: 0))
            path.addLine(to: CGPoint(x: rect.maxX, y: height))
            path.addLine(to: CGPoint(x: rect.midX + thickness, y: height))
            path.addLine(to: CGPoint(x: rect.midX + thickness, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.midX - thickness, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.midX - thickness, y: height))
            path.addLine(to: CGPoint(x: 0, y: height))
            path.closeSubpath()
        }
    }
}

struct ContentView: View {
    @State private var headHeight = 0.5
    @State private var shaftWidth = 0.5
    
    var body: some View {
        Arrow(headHeight: headHeight, shaftWidth: shaftWidth)
            .fill(.blue)
            .onTapGesture {
                withAnimation {
                    headHeight = Double.random(in: 0.2...0.8)
                    shaftWidth = Double.random(in: 0.2...0.8)
                }
            }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

SwiftUI drawing challenge #3

The third and final challenge is to 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.

Since we’ve already created the ColorCyclingCircle during day 44, we’ll be recycling that code and modifying it for this challenge.

//
//  ContentView.swift
//  DrawingChallengeDay46
//


import SwiftUI

struct ColorCyclingRectangle: View {
    var amount = 0.0
    var steps = 100
    
    
    // These variables will function as our UnitPoint. We previously said .top/.bottom. Using these variables make the start and end points customizable.
    var gradientStartX = 0.5
    var gradientStartY = 0.0
    
    var gradientEndX = 0.5
    var gradientEndY = 1.0
    
    var body: some View {
        ZStack {
            ForEach(0..<steps) { value in
                Rectangle()
                    .inset(by: Double(value))
                    .strokeBorder(
                        LinearGradient(
                            gradient: Gradient(colors: [
                                color(for: value, brightness: 1),
                                color(for: value, brightness: 0.5)
                            ]),
                            startPoint: UnitPoint(x: gradientStartX, y: gradientStartY),
                            endPoint: UnitPoint(x: gradientEndX, y: gradientEndY)
                    ),
                    lineWidth: 2
                )
            }
        }
        .drawingGroup() // This modifier tells SwiftUI to render the content of the view into an off screen image, before putting it back on the screen as a single piece of rendered output. Behind the scenes, this is not powered by Core Animation. Instead, it's powered by Apple's graphics API Metal.
    }
    
    func color(for value: Int, brightness: Double) -> Color {
        var targetHue = Double(value) / Double(steps) + amount // If we are circle 25 of 100, for example, that is our targetHue
        
        if targetHue > 1 {
            targetHue -= 1
        }
        
        return Color(hue: targetHue, saturation: 1, brightness: brightness)
    }
}

struct ContentView: View {
    @State private var colorCycle = 0.0
    @State var gradientStartX = 0.5
    @State var gradientStartY = 0.0
    @State var gradientEndX = 0.5
    @State var gradientEndY = 1.0
    
    var body: some View {
        VStack {
            ColorCyclingRectangle(amount: colorCycle, gradientStartX: gradientStartX, gradientStartY: gradientStartY, gradientEndX: gradientEndX, gradientEndY: gradientEndY)
                .frame(width: 300, height: 300)
            
            HStack {
                Text("Color")
                Slider(value: $colorCycle)
            }
            
            HStack {
                Text("Start X")
                Slider(value: $gradientStartX)
            }
            
            HStack {
                Text("Start Y")
                Slider(value: $gradientStartY)
            }
            
            HStack {
                Text("End X")
                Slider(value: $gradientEndX)
            }
            
            HStack {
                Text("End Y")
                Slider(value: $gradientEndY)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Wrap up

And that’s it for day 46 and the drawing technique project. The next two days will be consolidation days and they’re well timed. We’ve learned some great skills over the past few projects, but the difficulty has definitely ramped up and it’s good to take a few days to go over what we’ve learned and ensure it really landed. Onwards and upwards from here, as we’re moving towards the halfway point!

Darryl

Hi! My name is Darryl and this is my personal blog where I write about my journey as I learn programming! You'll also find articles about other things that interest me including games, tech and anime.

Post navigation

Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

100 Days of SwiftUI – Day 5 – Conditional Statements

100 Days of SwiftUI – Day 69

100 Days of SwiftUI – Day 7 – Functions

100 Days of SwiftUI – Day 48