SwiftUI Shapes
Main Idea
SwiftUI implements Shape as a protocol with a single required method: given the following rectangle, what path do you want to draw?
This will still create and return a path just like using a raw path directly, but because we’re handed the size the shape will be used at we know exactly how big to draw our path – we no longer need to rely on fixed coordinates.
The key to understanding the difference between Path and Shape is reusability: paths are designed to do one specific thing, whereas shapes have the flexibility of drawing space and can also accept parameters to let us customize them further.
// drawing a triangle
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
}
}
struct ContentView: View {
var body: some View {
Triangle()
.stroke(.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 300)
}
}
// drawing a arc
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
func path(in rect: CGRect) -> Path {
let rotationAdjustment = Angle.degrees(90)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise)
return path
}
}
struct ContentView: View {
var body: some View {
Arc(startAngle: .degrees(0), endAngle: .degrees(110), clockwise: true)
.stroke(.blue, lineWidth: 10)
.frame(width: 300, height: 300)
}
}
// InsettableShape
struct Arc: Shape, InsettableShape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
var insetAmount = 0.0
func inset(by amount: CGFloat) -> some InsettableShape {
var arc = self
arc.insetAmount += amount
return arc
}
func path(in rect: CGRect) -> Path {
let rotationAdjustment = Angle.degrees(90)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2 - insetAmount, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise)
return path
}
}
struct ContentView: View {
var body: some View {
Arc(startAngle: .degrees(-90), endAngle: .degrees(90), clockwise: true)
.strokeBorder(.blue, lineWidth: 40)
}
}
struct Arrow: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.midY / 2))
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY / 2))
return path
}
}
struct ContentView: View {
@State private var lineWidth = 10.0
var body: some View {
VStack {
withAnimation{
Arrow()
.stroke(.red, style: StrokeStyle(lineWidth: CGFloat(lineWidth), lineCap: .round, lineJoin: .round))
.frame(width: 200, height: 300)
}
Slider(value: $lineWidth, in: 1...50)
}
}
}