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 CGAffineTransform
, ImagePaint
, drawingGroup()
, 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!
100 Days of SwiftUI – Day 44