Better performance with canvas in SwiftUI
It is said that use of Canvas to create complex shapes can provide better performance in SwiftUI. This article compares performance of scrolling through multiple instances of the same card pattern created using shape, canvas or image.
Create a card pattern using a Shape
All of the patterns are build using the Diamond shape created in Create patterns
from basic shapes in SwiftUI. The DiamondShape conforms to the Shape protocol
and implements the path function to create a diamond shape inside the containing
rectangle. There is one correction to the original diamond shape, in the
offsetPoint
function, to adjust for the origin of the containing rectangle not being
at (0,0).
1struct DiamondShape: Shape {
2 func path(in rect: CGRect) -> Path {
3 let size = min(rect.width, rect.height)
4 let xOffset = (rect.width > rect.height) ? (rect.width - rect.height) / 2.0 : 0.0
5 let yOffset = (rect.height > rect.width) ? (rect.height - rect.width) / 2.0 : 0.0
6
7 func offsetPoint(p: CGPoint) -> CGPoint {
8 return CGPoint(x: p.x + xOffset + rect.origin.x, y: p.y+yOffset + rect.origin.y)
9 }
10
11 let path = Path { path in
12 path.move(to: offsetPoint(p: CGPoint(x: 0, y: (size * 0.50))))
13 path.addQuadCurve(to: offsetPoint(p: CGPoint(x: (size * 0.50), y: 0)),
14 control: offsetPoint(p: CGPoint(x: (size * 0.40), y: (size * 0.40))))
15 path.addQuadCurve(to: offsetPoint(p: CGPoint(x: size, y: (size * 0.50))),
16 control: offsetPoint(p: CGPoint(x: (size * 0.60), y: (size * 0.40))))
17 path.addQuadCurve(to: offsetPoint(p: CGPoint(x: (size * 0.50), y: size)),
18 control: offsetPoint(p: CGPoint(x: (size * 0.60), y: (size * 0.60))))
19 path.addQuadCurve(to: offsetPoint(p: CGPoint(x: 0, y: (size * 0.50))),
20 control: offsetPoint(p: CGPoint(x: (size * 0.40), y: (size * 0.60))))
21 path.closeSubpath()
22 }
23 return path
24 }
25}
The DiamondCardView
is created by arranging rows and columns of the diamond shapes
using vertical and horizontal stacks.
1struct DiamondCardView: View {
2 var rows :Int
3 var cols: Int
4
5 var body: some View {
6 GeometryReader { gr in
7 let width = gr.size.width / CGFloat(cols)
8 let height = gr.size.height / CGFloat(rows)
9
10 VStack(spacing:0) {
11 ForEach(0..<rows) { i in
12 let rowCols = (i%2==0) ? cols : cols - 1
13 HStack(spacing:0) {
14 Group {
15 ForEach(0..<rowCols) { _ in
16 DiamondShape()
17 .fill(Color(red: 250/255, green: 100/255, blue: 90/255))
18 .frame(width: width, height: height)
19 }
20 }
21 }
22 }
23 }
24 }
25 }
26}
The card pattern is further enhanced by adding borders and rounding the corners.
1struct SingleShapeView: View {
2 var body: some View {
3 ZStack {
4 Color(red: 214/255, green: 232/255, blue: 248/255)
5 .edgesIgnoringSafeArea(.all)
6
7 VStack {
8 Text("Card from Shape")
9 .font(.title)
10 .fontWeight(.bold)
11 .padding(.vertical, 30)
12
13 Group {
14 DiamondCardView(rows: 21, cols: 7)
15 .padding(40)
16 .frame(width: 250, height: 430)
17 .background(Color(red: 30/255, green: 40/255, blue: 60/255))
18 .cornerRadius(30)
19 .overlay(RoundedRectangle(cornerRadius: 40)
20 .stroke(Color(red: 0.93, green: 0.94, blue: 0.77), lineWidth: 50)
21 .cornerRadius(30))
22 .overlay(RoundedRectangle(cornerRadius: 20)
23 .stroke(Color(red: 250/255, green: 100/255, blue: 90/255), lineWidth: 20)
24 .cornerRadius(15)
25 .padding(25))
26 .overlay(RoundedRectangle(cornerRadius: 30)
27 .stroke(.black, lineWidth: 2)
28 .cornerRadius(30))
29 }
30 .rotationEffect(.degrees(10))
31
32 Spacer()
33 }
34 }
35 }
36}
Define card pattern using Shapes
Create the card pattern using Canvas
As discussed in Using canvas in SwiftUI, the Canvas view provides a mechanism
to draw in SwiftUI in an efficient manner. The canvas uses GraphicsContext and size
to draw immediately within the containing rectangle. The same card pattern as above
is created using a canvas to layout the individual diamond shapes. In addition, the
same DiamondShape
is used in the Canvas by accessing the path function of the shape
and passing the containing rectangle.
1struct SingleCanvasView: View {
2 var body: some View {
3 ZStack {
4 Color(red: 214/255, green: 232/255, blue: 248/255)
5 .edgesIgnoringSafeArea(.all)
6
7 VStack {
8 Text("Card from Canvas")
9 .font(.title)
10 .fontWeight(.bold)
11 .padding(.vertical, 30)
12
13 Canvas { context, size in
14 let w = size.width
15 let h = size.height
16 let rows = 21
17 let cols = 7
18
19 for r in 0..<rows {
20 for c in 0..<cols {
21 let x = w/CGFloat(cols) * CGFloat(c)
22 let y = h/CGFloat(rows) * CGFloat(r)
23 context.fill(
24 DiamondShape().path(in: CGRect(
25 x: x,
26 y: y,
27 width: w/CGFloat(cols),
28 height: h/CGFloat(rows))),
29 with: .color(red: 250/255, green: 100/255, blue: 90/255))
30 }
31 }
32 }
33 .padding(40)
34 .background(Color(red: 30/255, green: 40/255, blue: 60/255))
35 .frame(width: 250, height: 430)
36 .cornerRadius(30)
37 .overlay(RoundedRectangle(cornerRadius: 40)
38 .stroke(Color(red: 0.93, green: 0.94, blue: 0.77), lineWidth: 50)
39 .cornerRadius(30))
40 .overlay(RoundedRectangle(cornerRadius: 20)
41 .stroke(Color(red: 250/255, green: 100/255, blue: 90/255), lineWidth: 20)
42 .cornerRadius(15)
43 .padding(25))
44 .overlay(RoundedRectangle(cornerRadius: 30)
45 .stroke(.black, lineWidth: 2)
46 .cornerRadius(30))
47 .rotationEffect(.degrees(10))
48
49 Spacer()
50 }
51 }
52 }
53}
Define card pattern using Canvas
ScrollView with cards from Shape
Individually, the above views display OK and there appears to be no performance
differences. A ScrollView is created containing 50 of the DiamondCardView
to stress
out the system and look for performance issues.
1struct MultipleShapeView: View {
2 var body: some View {
3 ZStack {
4 Color(red: 214/255, green: 232/255, blue: 248/255)
5 .edgesIgnoringSafeArea(.all)
6
7 ScrollView {
8 ForEach(1...50, id: \.self) { _ in
9 CardShapeView()
10 }
11 }
12 .navigationTitle("50 Shape Cards")
13 }
14 }
15}
16
17
18struct CardShapeView: View {
19 var body: some View {
20 DiamondCardView(rows: 7, cols: 21)
21 .padding(30)
22 .frame(width: 300, height: 200)
23 .background(Color(red: 30/255, green: 40/255, blue: 60/255))
24 .cornerRadius(30)
25 .overlay(RoundedRectangle(cornerRadius: 40)
26 .stroke(Color(red: 0.93, green: 0.94, blue: 0.77), lineWidth: 50)
27 .cornerRadius(30))
28 .overlay(RoundedRectangle(cornerRadius: 20)
29 .stroke(Color(red: 250/255, green: 100/255, blue: 90/255), lineWidth: 10)
30 .cornerRadius(15)
31 .padding(20))
32 .overlay(RoundedRectangle(cornerRadius: 30)
33 .stroke(.black, lineWidth: 2)
34 .cornerRadius(30))
35 }
36}
Multiple card pattern defined using using Shape
ScrollView with cards from Canvas
A similar ScrollView is created containing 50 of the CardCanvasView
to stress out
the system and look for performance issues.
1struct MultipleCanvasView: View {
2 var body: some View {
3 ZStack {
4 Color(red: 214/255, green: 232/255, blue: 248/255)
5 .edgesIgnoringSafeArea(.all)
6
7 ScrollView {
8 ForEach(1...50, id: \.self) { i in
9 CardCanvasView()
10 }
11 }
12 .navigationTitle("50 Canvas Card")
13 }
14 }
15}
16
17
18struct CardCanvasView: View {
19 var body: some View {
20 VStack {
21 Canvas { context, size in
22 let w = size.width
23 let h = size.height
24 let rows = 7
25 let cols = 21
26
27 for r in 0..<rows {
28 for c in 0..<cols {
29 let x = w/CGFloat(cols) * CGFloat(c)
30 let y = h/CGFloat(rows) * CGFloat(r)
31 context.fill(
32 DiamondShape().path(in: CGRect(x: x, y: y, width: w/CGFloat(cols), height: h/CGFloat(rows))),
33 with: .color(red: 250/255, green: 100/255, blue: 90/255))
34 }
35 }
36 }
37 .padding(30)
38 .frame(width: 300, height: 200)
39 .background(Color(red: 30/255, green: 40/255, blue: 60/255))
40 .cornerRadius(30)
41 .overlay(RoundedRectangle(cornerRadius: 40)
42 .stroke(Color(red: 0.93, green: 0.94, blue: 0.77), lineWidth: 50)
43 .cornerRadius(30))
44 .overlay(RoundedRectangle(cornerRadius: 20)
45 .stroke(Color(red: 250/255, green: 100/255, blue: 90/255), lineWidth: 10)
46 .cornerRadius(15)
47 .padding(20))
48 .overlay(RoundedRectangle(cornerRadius: 30)
49 .stroke(.black, lineWidth: 2)
50 .cornerRadius(30))
51 }
52 }
53}
Multiple card pattern defined using Canvas
ScrollView with cards from Image
Finally, a ScrollView is created containing 50 images for the cards to compare performance to that of using the shapes and the canvas. The image is created from a screenshot of one of the cards above and added to the assets.
1struct MultipleImageView: View {
2 var body: some View {
3 ZStack {
4 Color(red: 214/255, green: 232/255, blue: 248/255)
5 .edgesIgnoringSafeArea(.all)
6
7 ScrollView {
8 ForEach(1...50, id: \.self) { _ in
9 Image("card-background")
10 .resizable()
11 .scaledToFill()
12 .clipShape(RoundedRectangle(cornerRadius: 40))
13 .frame(width: 310, height: 200)
14 }
15 }
16 .navigationTitle("50 Image Card")
17 }
18 }
19}
Card pattern image
Multiple card with image as background
Review performance
There are a number of ways of measuring performance, including automated performance tests, which I'll dig into in another article. In this app we'll just run the application and look at the CPU and Memory usage in Xcode. It can be seen that the ScrollView with the 50 cards created using shapes for each diamond and laying them out in Stacks uses significantly more memory to load. It also uses significantly more CPU when scrolling the view up and down. The cards created with canvas use significantly less memory to load and less CPU usage when scrolling up and down. Using an image for the cards performs the best, but only slightly better than use of canvas.
Increased memory and CPU usage with card pattern created with Shape
Increased memory usage with card pattern created with Shape
Conclusion
A ScrollView containing 50 instances of the same card may not be the most realistic scenario, but it illustrates the point of performance differences in creating these views. Using the canvas to layout multiple shapes such as diamonds results in much better performance than laying out multiple shapes in a view. This can be seen in the responsiveness of the scrolling and the memory and CPU usage. If these shape in the app are static, then use of an image provides slightly better performance than use of canvas. The use of canvas performed almost as well as the use of images, so if the content needs to be dynamic, then a canvas maybe the best option.