Using Calculated Geometric Properties: How To Draw Complex Shapes in SwiftUI
The path command in SwiftUI is nothing more than the pen or vector tool in Figma and Sketch. You can grab it to draw any shape you could imagine. Drawing basic shapes with a straight point in SwiftUI is easy but it becomes mind-bending when you need to sketch complex shapes consisting of straight and curved points using fixed geometric values. Luckily, the Core Graphics Framework makes it possible to use calculated geometric properties (minX, midX, maxX, minY, midY, maxY) of a rectangle to make the process easier.
This article is the first part of a series of articles that cover how to draw custom shape outlines with SwiftUI. In this article, you will discover how to construct basic and complex 2D shape outlines in SwiftUI using minimum, medium and maximum coordinates of rectangles. Using these values removes the pain of guessing fixed values to draw shapes. The techniques, tips, and tricks you learn from this post will assist you in drawing complex shapes in SwiftUI with your eyes closed.
It may sound like a brag, but continue reading to find out how to draw the Stream logo mark in SwiftUI. Grab the code from GitHub.
What Are Calculated Geometric Properties?
Calculated geometric properties in Core Graphics allow you to specify the size (width and height) and origin of a 2D outline using minimum, medium, and maximum values. This removes the tedious work of using fixed values for specifying origin and size when drawing custom shapes.
Understanding the iOS Coordinate System
Traditionally, the origin of a rectangle is positioned at the bottom left in most systems like macOS. iOS, on the other hand, has a flipped geometry. So the origin is situated at the top-left corner of a rectangle (minX, minY). Having this at the back of your mind, you can draw custom shapes starting from the origin in the clockwise or anticlockwise directions on iOS.
Launch Xcode and Create a Blank SwiftUI Project
Create a new SwiftUI app in Xcode and choose any name you prefer. This demo project uses the product name CalculatedGeometricProperties.
Using an Image As Drawing Template
To sketch the Stream logo, you will use the (minX, midX, maxX, minY, midY, maxY) properties of a rectangle instead of fixed values. This makes it comprehensive to create custom shapes. To understand how this works visually, you will add the SVG image template above to the Xcode assets library and pull it to the content view to guide you draw the shape easily.
How to Draw Custom Shapes in SwiftUI
You can create a path in SwiftUI in several ways. Let's add it as a function that accepts CGRect (origin: (x: 0.0, y: 0.0), size(width: 1.0, height: 1.0)) and returns a path. In this way, you will have more customization options.
struct StreamLogo: Shape { }
2. Add the variable var controlPoint: Double inside the struct.
3. Add the method path that takes CGRect and returns a path.
func path(in rect: CGRect) -> Path { var pencil = Path() return pencil }
4. Inside this method, you should create an instance of the path var pencil = Path(). You can think of this as drawing on physical paper. To draw on paper, for example, you need a pen or pencil.
5. In the ContentView struct, add the variable var controlPoint = 0.0 to the declaration section. In the body of the content view, add a ZStack to contain the drawing template cgrect-min-mid-max and an instance of the custom shape.
ZStack {
Image("cgrect-min-mid-max")
.opacity(0.25)
StreamLogo(controlPoint: controlPoint)
.stroke(.blue, style: StrokeStyle(lineWidth: 10,
lineCap: .round, lineJoin: .round))
.offset(x: 20, y: 16)
}
When you draw the shape, it adds the fill by default which may not be what you want. So, add the stroke modifier to remove the fill. Also, iOS draws the shape at the top-left corner of the screen. Use the offset modifier to move it away from the top-left corner. An ideal option is to use Geometry Reader.
6. Now, you have Path(), the paper to draw on, and the instance of the path (pencil) to draw. You can sketch the logo starting from anywhere on the drawing template. To simplify the process, you should begin from the top-left (minX, minY) and move in a clockwise direction.
Recommended by LinkedIn
7. Move to the origin to start drawing pencil.move(to: CGPoint(x: rect.minX, y: rect.minY)). Draw the first point at the origin (minX, minY) using pencil.addLine(to: CGPoint(x: rect.minX, y: rect.minY)).
8. Move in a clockwise direction toward midX and specify the second point pencil.addLine(to: CGPoint(x: rect.midX - 20, y: rect.minY + 8)). Since this point is on the left of midX, you can deduct 20 from midX. On the y-axis, positive values go down and negative values go up. So, add 8 to minY to move the line down toward the right.
9. The next line must move up toward midX. Add the line pencil.addLine(to: CGPoint(x: rect.midX, y: rect.minY - 15)). At this point, subtracting 15 from minY makes the line moves up.
10. Draw the next line to be opposite of the previous pencil.addLine(to: CGPoint(x: rect.midX + 20, y: rect.minY + 8)).
11. The line that follows is the opposite of the first line from the origin. Add pencil.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)).
12. Draw a line from the previous one and downward using pencil.addLine(to: CGPoint(x: rect.maxX - 40, y: rect.maxY)). You should subtract 40 from maxX to make the line point toward the left.
13. Draw a horizontal line pencil.addLine(to: CGPoint(x: rect.minX + 40, y: rect.maxY)) and add 40 to minX so that the left and right sides of the figure become the same.
14. Finally, you should close the path. You can close it in two ways. In the first option, you can copy and paste the first control point below the last line path.addLine(to: CGPoint(x: rect.minX, y: rect.minY)). In the second option, you can use the method pencil.closeSubpath().
15. Apply the frame modifier to change the height of the shape .frame(width: 184, height: 80) in the content view.
16. Remove the image template Image("cgrect-min-mid-max") and increase the line width of the shape.
Putting All Together
Replace the content of ContentView.swift with the sample code below.
// ContentView.swift
// CalculatedGeometricProperties
// Created by amosGyamfi from getstream.io
// How to draw the Stream logo using the calculated CGRect
geometric properties minimum, medium, and maximum
import SwiftUI
struct StreamLogo: Shape {
var controlPoint: Double
func path(in rect: CGRect) -> Path {
var pencil = Path()
// INSTRUCTIONS
// 1. Move to an origin
pencil.move(to: CGPoint(x: rect.minX, y: rect.minY))
pencil.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
// 2. Move horizontally downward
pencil.addLine(to: CGPoint(x: rect.midX - 20, y: rect.minY +
8))
// 3.Move upward at an angle towards midX
pencil.addLine(to: CGPoint(x: rect.midX, y: rect.minY - 15))
// 4. Opposite of the previous line
pencil.addLine(to: CGPoint(x: rect.midX + 20, y: rect.minY +
8))
// 5. Opposite of the first line
pencil.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
// 6. Move downwards
pencil.addLine(to: CGPoint(x: rect.maxX - 40, y: rect.maxY))
// Move horizontally
pencil.addLine(to: CGPoint(x: rect.minX + 40, y: rect.maxY))
// Close the path
// Option 1
//path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
// Option 2
pencil.closeSubpath()
return pencil
}
}
struct ContentView: View {
var controlPoint = 0.0
var body: some View {
StreamLogo(controlPoint: controlPoint)
.stroke(.blue, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 184, height: 80)
.offset(x: 20, y: 16)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What Next?
To learn more about drawing custom shape outlines in SwiftUI, check out the Stream Developers YouTube Channel. You may subscribe to SwiftUI Tutorial#1 on YouTube. Download the sample code as a gist on GitHub. Enjoy!!!.
Senior Software UI Engineer (iOS/macOS) | YouTube Content Creator | Author
2yGreat job man!!!
Software Developer @ Globant | SDE-1 | LangChain | RAG Architecture | Prompt Engineering | GCP | PICT'21
2yAmos Gyamfi How can we draw curve shapes using path and CGRect?