100 Days of SwiftUI Day 44

100 Days of SwiftUI – Day 44

It’s day 44 of the 100 Days of SwiftUI! Yesterday, we learned about how drawing shapes and paths works in SwiftUI. Today, we’re expanding on that knowledge by covering CGAffineTransformImagePaintdrawingGroup(), and more. Let’s dive in!

Transforming shapes using CGAffineTransform and even-odd fills in SwiftUI

CGAffineTransform is something we’ll use often when needing to draw more complex paths. This feature lets us describe how a path should be rotated, scaled or sheared.

Even-odd fills are used to color overlapping paths. How they are colored depends on how many overlaps there are.

//
//  ContentView.swift
//  Drawing
//

import SwiftUI

struct Flower: Shape {
    var petalOffset = -20.0 // Move 20 points away from the center
    var petalWidth = 100.0 // How wide to make each petal
    
    func path(in rect: CGRect) -> Path {
        var path = Path() // This path will hold all the petals
        
        for number in stride(from: 0, to: Double.pi * 2, by: Double.pi / 8) { // Count in 1/8 of Pi each time
            let rotation = CGAffineTransform(rotationAngle: number) // Rotate the petal by 1/8 of Pi
            let position = rotation.concatenating(CGAffineTransform(translationX: rect.width / 2, y: rect.height / 2)) // Move the petal. Concatenating is adding this transform to the current transform, so rotate, then move. We want to move the width and height / 2, so halve of our drawing space.
            
            let originalPetal = Path(ellipseIn: CGRect(x: petalOffset, y: 0, width: petalWidth, height: rect.width / 2)) // Original petal, untransformed
            
            let rotatedPetal = originalPetal.applying(position) // rotates the petal by applying the position
            
            path.addPath(rotatedPetal)
        }
        
        return path
        
        // What we've created in this function is a bunch of rotating elipses.
    }
}

struct ContentView: View {
    @State private var petalOffset = -20.0
    @State private var petalWidth = 100.0
    
    var body: some View {
        VStack {
            Flower(petalOffset: petalOffset, petalWidth: petalWidth)
              // .stroke(.red, lineWidth: 1)
                .fill(.red, style: FillStyle(eoFill: true, antialiased: true)) //eoFill stands for Even/Odd fill. If Path's overlap, the first will be filled, the second won't, the third will, the fourth won't, etc.
            
            Text("OffSet")
            Slider(value: $petalWidth, in: -40...40)
                .padding([.horizontal, .bottom])
            
            Text("Width")
            Slider(value: $petalWidth, in: 0...100)
                .padding(.horizontal)
        }
    }
}

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

Creative borders and fills using ImagePaint

ImagePaint wraps up images in a way that gives us complete control over how it should be rendered. This means we can use images for borders and fills. ImagePaint takes up to 3 parameters: the image itself at the very least, a rectangle to be used inside the image as a source of our drawing and third, a scale for the image.

//
//  ContentView.swift
//  Drawing
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .frame(width: 300, height: 300)
            .border(ImagePaint(image: Image("black-sands"), sourceRect: CGRect(x: 0, y: 0.25, width: 1, height: 0.5), scale: 0.2), width: 50)
    }
}

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

Enable Metal rendering with drawingGroup()

When we’re drawing simple paths as we’ve seen up until now, this won’t have a performance impact on modern devices. However, this changes when drawing more complex paths.

The recent higher end Apple devices all support a 120Hz refresh rate on their display. This is the case with the iPhone, iPad Pro and even the MacBook Pro. This means that we should always aim for at least 120Hz within our apps. Anything less won’t look smooth.

//
//  ContentView.swift
//  Drawing
//

import SwiftUI

struct ColorCyclingCircle: View {
    var amount = 0.0
    var steps = 100
    
    var body: some View {
        ZStack {
            ForEach(0..<steps) { value in
                Circle()
                    .inset(by: Double(value))
                    .strokeBorder(
                        LinearGradient(
                            gradient: Gradient(colors: [
                                color(for: value, brightness: 1),
                                color(for: value, brightness: 0.5)
                            ]),
                            startPoint: .top,
                            endPoint: .bottom
                    ),
                    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
    var body: some View {
        VStack {
            ColorCyclingCircle(amount: colorCycle)
                .frame(width: 300, height: 300)
            
            Slider(value: $colorCycle)
        }
    }
}

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

Wrap up

And that’s it for day 44! Tomorrow, we’ll be looking at drawing special effects and animations. That is on the advanced side of drawing in SwiftUI. High time to recharge. We’ll be back tomorrow!

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 43

100 Days of SwiftUI – Day 99

100 Days of SwiftUI – Day 60

100 Days of SwiftUI – Day 97