SwiftUI provides a robust set of built-in shapes such as Rectangle
, Circle
, and RoundedRectangle
. However, the real power of SwiftUI lies in its ability to create custom shapes tailored to your specific design needs. By using the Path
and Shape
protocols, developers can create anything from simple polygons to complex, intricate designs.
What are Paths and Shapes in SwiftUI?
In SwiftUI, the Path
and Shape
protocols are fundamental for drawing vector-based graphics:
- Path: Represents the outline of a shape as a series of drawing commands (e.g., move to, line to, curve to). It’s the raw data structure that defines the visual appearance of a shape.
- Shape: A protocol that
View
s can conform to. AShape
is defined by itspath(in:)
method, which specifies how to draw the shape within a given rectangle. By conforming to theShape
protocol, a custom type can be used anywhere aView
is expected, inheriting capabilities like applying modifiers (e.g.,fill
,stroke
).
Why Create Custom Shapes?
- Design Flexibility: Tailor UI elements to perfectly match design specifications.
- Unique UI Elements: Create distinctive and memorable user interfaces.
- Enhanced User Experience: Implement complex animations and transitions using custom shapes.
How to Build Custom SwiftUI Shapes
Here’s how to create custom shapes in SwiftUI using Path
and Shape
:
Step 1: Create a Shape Struct
First, create a struct that conforms to the Shape
protocol. This struct will define the shape’s characteristics and how it should be drawn.
import SwiftUI
struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
return path
}
}
Explanation:
- We create a
Triangle
struct that conforms to theShape
protocol. - The
path(in:)
method is required by theShape
protocol. It defines the drawing instructions within the provided rectanglerect
. - A
Path
instance is created. The path is built using a series of drawing commands:move(to:)
sets the starting point of the path.addLine(to:)
draws a line from the current point to the specified point.
- Finally, the method returns the constructed
Path
.
Step 2: Use the Custom Shape in Your View
Now, you can use the custom shape in your SwiftUI views like any other built-in shape.
struct ContentView: View {
var body: some View {
Triangle()
.fill(.blue)
.frame(width: 200, height: 150)
.padding()
}
}
Explanation:
- A
ContentView
is created. - The
Triangle
shape is instantiated. - The
fill(.blue)
modifier sets the fill color of the triangle to blue. - The
frame(width:height:)
modifier sets the size of the frame containing the triangle. - The
padding()
modifier adds padding around the shape.
Example 2: Creating a Rounded Polygon
Let’s create a more complex shape, a rounded polygon with a specified number of sides and corner radius.
struct RoundedPolygon: Shape {
let sides: Int
let cornerRadius: CGFloat
func path(in rect: CGRect) -> Path {
guard sides >= 3 else { return Path() }
let center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 2
var path = Path()
for i in 0..<sides {
let angle = CGFloat(i) * (360 / CGFloat(sides)) * .pi / 180
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
let point = CGPoint(x: x, y: y)
if i == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.closeSubpath()
// Apply corner radius
return path.applying(CGAffineTransform(translationX: -rect.origin.x, y: -rect.origin.y))
.roundedCorners(radius: cornerRadius)
.applying(CGAffineTransform(translationX: rect.origin.x, y: rect.origin.y))
}
}
extension Path {
func roundedCorners(radius: CGFloat) -> Path {
var path = self
guard let currentPoint = path.currentPoint else { return path }
let boundingBox = path.boundingRect
let insetRect = boundingBox.insetBy(dx: radius, dy: radius)
let minX = insetRect.minX
let minY = insetRect.minY
let maxX = insetRect.maxX
let maxY = insetRect.maxY
let roundedRect = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
// Remove all path segments.
var tempPath = Path()
// swiftlint:disable:next identifier_name
func roundCorner(_ point: CGPoint, _ corner: CGPoint) -> CGPoint {
return CGPoint(
x: point.x + (corner.x - point.x).normalized * radius,
y: point.y + (corner.y - point.y).normalized * radius)
}
func moveAndDraw(from point: CGPoint, to cornerPoint: CGPoint) {
// Move to start of curved line segment in relative point form.
let relativeToOriginalPoint: CGPoint = CGPoint(x: (point.x - currentPoint.x) / radius,
y: (point.y - currentPoint.y) / radius)
let curve: CGPoint = roundCorner(point, cornerPoint)
tempPath.move(to: curve)
}
return tempPath
}
}
extension CGFloat {
/// A normalized float.
var normalized: CGFloat {
guard self != 0 else { return 0 }
return self / abs(self)
}
}
Explanation:
RoundedPolygon
struct is created with customizablesides
andcornerRadius
.- The
path(in:)
method calculates the points of the polygon using trigonometric functions. - The
applying(_:)
modifier applies a transform to shift path.
Usage:
struct ContentView: View {
var body: some View {
RoundedPolygon(sides: 6, cornerRadius: 15)
.fill(.orange)
.frame(width: 200, height: 200)
.padding()
}
}
Example 3: Creating a Star Shape with Animated Properties
Let’s create a star shape where you can customize the number of points, and inner radius ratio. Make it animatable to change the pointCount using AnimatableData
.
import SwiftUI
struct Star: Shape, Animatable {
var pointCount: Int
var innerRadiusRatio: CGFloat
var animatableData: AnimatablePair {
get {
AnimatablePair(Double(pointCount), Double(innerRadiusRatio))
}
set {
pointCount = Int(newValue.first)
innerRadiusRatio = CGFloat(newValue.second)
}
}
func path(in rect: CGRect) -> Path {
guard pointCount >= 2 else { return Path() }
let center = CGPoint(x: rect.midX, y: rect.midY)
let outerRadius = min(rect.width, rect.height) / 2
let innerRadius = outerRadius * innerRadiusRatio
var path = Path()
for i in 0..<pointCount * 2 {
let radius = i.isMultiple(of: 2) ? outerRadius : innerRadius
let angle = CGFloat(i) * (360 / CGFloat(pointCount * 2)) * .pi / 180
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
let point = CGPoint(x: x, y: y)
if i == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.closeSubpath()
return path
}
}
Usage Example
struct ContentView: View {
@State private var starPoints: Int = 5
@State private var innerRatio: CGFloat = 0.4
var body: some View {
VStack {
Star(pointCount: starPoints, innerRadiusRatio: innerRatio)
.fill(.red)
.frame(width: 200, height: 200)
.padding()
Slider(value: $innerRatio, in: 0.1...0.9, step: 0.1) {
Text("Inner Ratio: \(innerRatio, specifier: "%.1f")")
}
Stepper("Star Points: \(starPoints)", value: $starPoints, in: 3...12)
.padding()
}
.padding()
}
}
Conclusion
Creating custom shapes with Path
and Shape
in SwiftUI allows for unparalleled design flexibility. By mastering these fundamental concepts, developers can bring complex and unique UI elements to life. Whether it’s simple geometric figures or intricate, animated designs, SwiftUI’s shape-drawing capabilities empower you to create outstanding user experiences.