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
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
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
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
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.