Using canvas in SwiftUI
The canvas view provides a mechanism to draw in SwiftUI. The canvas takes GraphicsContext and size as parameters to allow immediate mode drawing within the containing frame. Here we draw system shapes as well as freeform shapes using path structure.
Draw Ellipse in a Canvas
This view contains three Canvas views in a VStack. The first Canvas uses the
GraphicsContext to fill an Ellipse with the color blue. The ellipse is got by
initializing the Path on the canvas with the ellipseIn
CGRect parameter and
specifying the canvas containing rectangle. This creates a path as an ellipse
inscribed within the given rectangle.
The second canvas separates out the definition of the path to a separate function, but still fills in the color blue for the ellipse. The third canvas adds some padding by setting an offset for the path CGRect and reducing the width and height. Stroke is used on the GraphicsContext to outline the ellipse.
1struct ContentView: View {
2 var body: some View {
3 VStack {
4 Canvas { context, size in
5 context.fill(
6 Path(ellipseIn: CGRect(origin: .zero, size: size)),
7 with: .color(.blue))
8 }
9 .frame(width: 300, height: 150)
10 .border(Color.red)
11
12 Canvas { context, size in
13 context.fill(
14 path(in: CGRect(x:0 , y:0, width: size.width, height: size.height)),
15 with: .color(.blue))
16 }
17 .frame(width: 300, height: 150)
18 .border(Color.red)
19
20 Canvas { context, size in
21 context.stroke(
22 path(in: CGRect(x: size.width * 0.05,
23 y: size.height * 0.05,
24 width: size.width * 0.9,
25 height: size.height * 0.9)),
26 with: .color(.blue), lineWidth: 5)
27 }
28 .frame(width: 300, height: 150)
29 .border(Color.red)
30
31 Spacer()
32 }
33 }
34
35 func path(in rect: CGRect) -> Path {
36 let path = Path() { path in
37 path.addPath(Ellipse().path(in: rect))
38 }
39 return path
40 }
41}
Ellipse drawn in a canvas with separate path function
Draw a heart
The path function can get more complicated that simply using the path of a predefined
shape such as Rectangle or Ellipse. The separated heartPath
function is defined to
draw a heard shape within the enclosing rectangle. The logic is encapsulated in the
heartPath function to center the heard shape in the enclosing rectangle.
1struct ContentView: View {
2 var body: some View {
3 VStack {
4 Canvas { context, size in
5 context.fill(
6 heartPath(in: CGRect(origin: .zero, size: size)),
7 with: .color(.red))
8 }
9 .frame(width: 300, height: 200)
10 .border(Color.blue)
11
12 Spacer()
13 }
14 }
15
16 func heartPath(in rect: CGRect) -> Path {
17 let size = min(rect.width, rect.height)
18 let xOffset = (rect.width > rect.height) ? (rect.width - rect.height) / 2.0 : 0.0
19 let yOffset = (rect.height > rect.width) ? (rect.height - rect.width) / 2.0 : 0.0
20
21 func offsetPoint(p: CGPoint) -> CGPoint {
22 return CGPoint(x: p.x + xOffset, y: p.y+yOffset)
23 }
24 let path = Path() { path in
25 path.move(to: offsetPoint(p: (CGPoint(x: (size * 0.50), y: (size * 0.25)))))
26 path.addCurve(to: offsetPoint(p: CGPoint(x: 0, y: (size * 0.25))),
27 control1: offsetPoint(p: CGPoint(x: (size * 0.50), y: (-size * 0.10))),
28 control2: offsetPoint(p: CGPoint(x: 0, y: 0)))
29 path.addCurve(to: offsetPoint(p: CGPoint(x: (size * 0.50), y: size)),
30 control1: offsetPoint(p: CGPoint(x: 0, y: (size * 0.60))),
31 control2: offsetPoint(p: CGPoint(x: (size * 0.50), y: (size * 0.80))))
32 path.addCurve(to: offsetPoint(p: CGPoint(x: size, y: (size * 0.25))),
33 control1: offsetPoint(p: CGPoint(x: (size * 0.50), y: (size * 0.80))),
34 control2: offsetPoint(p: CGPoint(x: size, y: (size * 0.60))))
35 path.addCurve(to: offsetPoint(p: CGPoint(x: (size * 0.50), y: (size * 0.25))),
36 control1: offsetPoint(p: CGPoint(x: size, y: 0)),
37 control2: offsetPoint(p: CGPoint(x: (size * 0.50), y: (-size * 0.10))))
38 }
39 return path
40 }
41}
Path to draw a heart in a canvas
Star shape in a Canvas
This takes the path of the Star shape used in Star with rounded corners in
SwiftUI. The struct for the Segment
is the same and is used to create the
required number of segments for the star. The function starPath
takes a parameter
to specify the number of points in the star.
1func Cartesian(length:Double, angle:Double) -> CGPoint {
2 return CGPoint(x: length * cos(angle), y: length * sin(angle))
3}
1struct Segment {
2 let outerCenter: CGPoint
3 let outerAngle: Double
4 let outerRadius: Double
5 let innerCenter: CGPoint
6 let innerAngle: Double
7
8 var line: CGPoint {
9 get {
10 let pt = Cartesian(length: outerRadius, angle: outerStartAngle)
11 return CGPoint(x: pt.x + outerCenter.x, y: pt.y + outerCenter.y)
12 }
13 }
14
15 var line2: CGPoint {
16 get {
17 let pt = Cartesian(length: outerRadius, angle: innerStartAngle)
18 return CGPoint(x: pt.x + innerCenter.x, y: pt.y + innerCenter.y)
19 }
20 }
21
22 var outerStartAngle: Double {
23 get { self.outerAngle - (Double.pi * (0.45)) }
24 }
25 var outerEndAngle: Double {
26 get { self.outerAngle + (Double.pi * (0.45)) }
27 }
28
29 var innerStartAngle: Double {
30 get { self.innerAngle - (Double.pi * (0.7)) }
31 }
32 var innerEndAngle: Double {
33 get { self.innerAngle + (Double.pi * (0.7)) }
34 }
35}
1struct ContentView: View {
2 var body: some View {
3 VStack {
4 Canvas { context, size in
5 context.fill(
6 starPath(in: CGRect(origin: .zero, size: size), points: 5),
7 with: .color(.green))
8 }
9 .frame(width: 300, height: 300)
10
11 Spacer()
12 .frame(height: 80)
13
14 HStack {
15 Canvas { context, size in
16 context.fill(
17 starPath(in: CGRect(origin: .zero, size: size), points: 7),
18 with: .color(.yellow))
19 }
20 .frame(width: 150, height: 150)
21
22 Canvas { context, size in
23 context.fill(
24 starPath(in: CGRect(origin: .zero, size: size), points: 7),
25 with: .color(.yellow))
26 }
27 .frame(width: 100, height: 150)
28
29 Canvas { context, size in
30 context.fill(
31 starPath(in: CGRect(origin: .zero, size: size), points: 7),
32 with: .color(.yellow))
33 }
34 .frame(width: 50, height: 150)
35 }
36
37
38 Spacer()
39 }
40 }
41
42 func starPath(in rect: CGRect, points: Int) -> Path {
43 // centre of the containing rect
44 var center = CGPoint(x: rect.width/2.0, y: rect.height/2.0)
45 // Adjust center down for odd number of sides less than 8
46 if points%2 == 1 && points < 8 {
47 center = CGPoint(x: center.x, y: center.y * ((Double(points) * (-0.04)) + 1.3))
48 }
49
50 // radius of a circle that will fit in the rect with some padding
51 let outerRadius = (Double(min(rect.width,rect.height)) / 2.0) * 0.95
52 let innerRadius = outerRadius * 0.4
53 let offsetAngle = Double.pi * (-0.5)
54
55 let cornerRadius = (rect.width / Double(points)) * 0.07
56
57 var starSegments:[Segment] = []
58 for i in 0..<(points){
59 let angle1 = (2.0 * Double.pi/Double(points)) * Double(i) + offsetAngle
60 let outerPoint = Cartesian(length: outerRadius, angle: angle1)
61 let angle2 = (2.0 * Double.pi/Double(points)) * (Double(i) + 0.5) + offsetAngle
62 let innerPoint = Cartesian(length: (innerRadius), angle: (angle2))
63
64 let segment = Segment(
65 outerCenter: CGPoint(x: outerPoint.x + center.x,
66 y: outerPoint.y + center.y),
67 outerAngle: angle1,
68 outerRadius: cornerRadius,
69 innerCenter: CGPoint(x: innerPoint.x + center.x,
70 y: innerPoint.y + center.y),
71 innerAngle: angle2)
72 starSegments.append(segment)
73 }
74
75 let path = Path() { path in
76 for (n, seg) in starSegments.enumerated() {
77 n == 0 ? path.move(to: seg.line) : path.addLine(to: seg.line)
78 path.addArc(center: seg.outerCenter,
79 radius: seg.outerRadius,
80 startAngle: Angle(radians: seg.outerStartAngle),
81 endAngle: Angle(radians: seg.outerEndAngle),
82 clockwise: false)
83 path.addLine(to: seg.line2)
84 path.addArc(center: seg.innerCenter,
85 radius: seg.outerRadius,
86 startAngle: Angle(radians: seg.innerStartAngle),
87 endAngle: Angle(radians: seg.innerEndAngle),
88 clockwise: true)
89 }
90 path.closeSubpath()
91 }
92 return path
93 }
94}
Star shape in a canvas
Blob shape in Canvas
A random blob shape can also be created in a Canvas, using some of the code in How
to animate a Shape change in SwiftUI. The CubicSegment
structure is composed of
a destination point and two control points. The CreateRandomShape
function takes a
parameter to specify the number of segments in the shape and creates an array of
cubic segments within the enclosing rectangle. The blobPath
function creates a path
from the array of CubicSegment and returns this path.
1struct CubicSegment {
2 let point: CGPoint
3 let control1: CGPoint
4 let control2: CGPoint
5}
6
7func CreateRandomShape(segments n: Int) -> [CubicSegment] {
8 var segments:[CubicSegment] = []
9
10 let r = 0.5
11 let c = CGPoint(x: 0.5, y: 0.5)
12 let sectorAngle = 2.0 * Double.pi / Double(n)
13 var previousSectorIn = true
14 for i in 0..<n {
15 let segmentAngle = sectorAngle * Double.random(in: 0.7...1.3)
16 let angle = (sectorAngle * Double(i-0)) + segmentAngle
17 let radius = r * Double.random(in: 0.45...0.85)
18 let pt = Cartesian(length: radius, angle: angle)
19
20 let ctlAngle1 = angle - (segmentAngle * 0.75)
21 let ctlDistance1 = previousSectorIn ? radius*1.45 : radius*0.55
22 let ctl1 = Cartesian(length: ctlDistance1, angle: ctlAngle1)
23
24 let ctlAngle2 = angle - (segmentAngle * 0.25)
25 let ctlDistance2 = radius * Double.random(in: 0.55...1.45)
26 previousSectorIn = ctlDistance2 < radius
27 let ctl2 = Cartesian(length: ctlDistance2, angle: ctlAngle2)
28
29 let s:CubicSegment = CubicSegment(
30 point: CGPoint(x: pt.x + c.x, y: pt.y + c.y),
31 control1: CGPoint(x: ctl1.x + c.x, y: ctl1.y + c.y),
32 control2: CGPoint(x: ctl2.x + c.x, y: ctl2.y + c.y))
33 segments.append(s)
34 }
35 segments.append(segments[0])
36 return segments
37}
1struct ContentView: View {
2 let blob = CreateRandomShape(segments: 17)
3
4 var body: some View {
5 VStack {
6 Canvas { context, size in
7 context.fill(
8 blobPath(in: CGRect(origin: .zero, size: size), from: blob),
9 with: .color(.purple))
10 }
11 .frame(width: 300, height: 200)
12
13 Spacer()
14 }
15 }
16
17 func blobPath(in rect: CGRect, from segments: [CubicSegment]) -> Path {
18 func adjustPoint(p: CGPoint) -> CGPoint {
19 return CGPoint(x: p.x * rect.width, y: p.y * rect.height)
20 }
21
22 let path = Path() { path in
23 path.move(to: adjustPoint(p: segments[0].point))
24 for i in 1..<segments.count {
25 path.addCurve(to: adjustPoint(p: segments[i].point),
26 control1: adjustPoint(p: segments[i].control1),
27 control2: adjustPoint(p: segments[i].control2))
28 }
29 }
30 return path
31 }
32}
Random blob shape in a canvas
Conclusion
It is relatively straight forward to take Paths from shape views and draw the items directly in a Canvas. The canvas does not offer interactivity to individual elements but it can provide better performance for a complex drawing that involves dynamic data.