
Drawing graphics with Canvas
Learn how to use the Canvas view to render graphics within a SwiftUI app.
With iOS 15, Apple introduced Canvas
, a view type that can draw dynamic graphics in a SwiftUI app. When it comes to creating custom graphics and animations, this view allows for the handling of the rendering of several complex shapes more efficiently than composing multiple SwiftUI views, offering the flexibility needed for customization without neglecting the performance.
To render the content, Canvas
mainly relies on a GraphicsContext
object used to address the drawing commands and enable precise control over this kind of operation, whether paths, images, or text are being used.
In this reference article, we are going to explore how to integrateCanvas
to draw primitive figures by using the init(opaque:colorMode:rendersAsynchronously:renderer:)
constructor.
Canvas(
// 1. Opacity
opaque: false,
// 2. ColorMode
colorMode: .nonLinear,
// 3. Rendering mode
rendersAsynchronously: false
// 4. Render Closure
) { context, size in
// a. A Path
let rect = Path(roundedRect: CGRect(x: 0, y: 0, width: size.width - 20, height: size.height - 300), cornerRadius: 20)
// b. Fill the context with the path
context.fill(rect, with: .color(.red))
}
.border(.yellow)
Overview of the Canvas view constructor and a basic example
init(opaque:colorMode:rendersAsynchronously:renderer:)
takes the following parameters:
opaque
- A boolean value set to false by default, which makes the canvas fully opaque. When set to true, it becomes a non-opaque image, which can produce undefined results.

2. colorMode
- defines how colors are processed and displayed on the screen. By default it's set as ColorRenderingMode.nonLinear
. ColorRenderingMode
comes with three built-in modes:
nonLinear
- It uses the standard sRGB color space with gamma correction, which adjusts the brightness of colors to align with human visual perception, making sure that colors appear natural and consistent across different devices.linear
- It uses the standard sRGB color space without gamma correction, so that the colors are handled in their raw form, which results in more accuracy during operations like blending or applying lighting effects, but when displayed directly, they might appear darker than expected because the human eye perceives brightness non-linearly.extendedLinear
- It extends the capabilities of thelinear
sRGB color space by allowing color component values beyond the standard range [0, 1] enabling representation of a broader spectrum of colors, including those that are more vivid or intense than what standard sRGB can depict.
rendersAsynchronously
- A boolean value - set tofalse
by default - that allows presenting its contents to its parent view asynchronously.renderer
- A closure called whenever the content needs to be redrawn, which allows access to:
- a
GraphicsContext
, representing the context to draw in, by using its methods; - a
CGSize
, representing the current size of the canvas.
In this closure of the example, a path representing a red rectangle is being instantiated and inserted into the context and then being filled with the red color.
Canvas implementation in a SwiftUI view
To learn how to integrate the Canvas view in a SwiftUI view, we will create one where it is possible to manually draw graphics line by line using different colors.
First, let’s define a custom type called Line
. It will represent the line drawn on the canvas.
struct Line {
var points: [CGPoint]
var color: Color
}
This type will hold all the needed information to display the lines we want to draw on the canvas. It stores two properties:
- A collection of
CGPoint
- representing all the points composing the drawn line; - A
Color
- The color with which the line will be drawn.
Now, create a view representing the color selection bar that allows you to choose and change the color used while drawing.
struct ColorBarView: View {
// Binding to the selected color in the parent view
@Binding var selectedColor: Color
// Binding to the current line being drawn in the parent view
@Binding var currentLine: Line
// Predefined set of colors
var colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple, Color.black]
var body: some View {
HStack {
// Iterate over the colors to create color selection circles
ForEach(colors, id: \\.self) { color in
Circle()
.fill(color)
.frame(width: 30, height: 30)
.padding(5)
// 1. Detect the tap gesture
.onTapGesture {
// 2. Update the selected color and the current line's color when a circle is tapped
selectedColor = color
currentLine.color = color
}
// Add a gray stroke around the circle if it is the currently selected color
.overlay(
Circle()
.stroke(Color.gray, lineWidth: selectedColor == color ? 2 : 0)
)
}
}
}
}
The view binds two variables to its parent view:
- The selected color chosen by the user;
- The current line being drawn.
It works as follows:
- It detects the tap gesture.
- And, it updates the variables when one of the colors is being tapped.
In our ContentView
, we need a way to detect and collect where the user is moving on the screen while drawing; this information will be used for displaying each line on the canvas.
struct ContentView: View {
// State variable to store all the lines drawn by the user
@State private var lines: [Line] = []
// State variable to keep track of the current line being drawn
@State private var currentLine = Line(points: [], color: .black)
// State variable to store the currently selected color for drawing
@State private var selectedColor: Color = .black
var body: some View {
VStack {
Canvas { context, size in
// To be implemented
}
.gesture(
// 1. Detect the DragGesture on the screen
DragGesture()
.onChanged { value in
// 2. Append the current drag location to the current line's points
currentLine.points.append(value.location)
// 3. Add the updated current line to the lines array
lines.append(currentLine)
}
.onEnded { _ in
// 4. Reset the current line with the selected color after the drag ends
currentLine = Line(points: [], color: selectedColor)
}
)
// Color selection bar view
ColorBarView(selectedColor: $selectedColor, currentLine: $currentLine)
.padding()
}
.background(.ultraThinMaterial)
}
}
- Create a
DragGesture
to detect and store the dragging movement on the screen when the user is drawing the lines. - Whenever the user drags on the screen, touch locations are captured by
DragGesture
and appended to thecurrentLine.points
, a state variable that keeps track of the current line being drawn. - This line is then added to the
lines
array, which stores all lines to be rendered to display the out-coming drawing. - When the dragging is over, the
currentLine
is reset with the selected color, ready for the next drawing.
Now that we have all the information needed to display the drawings, we are ready to use them to implement the Canvas
logic.
struct ContentView: View {
// State variable to store all the lines drawn by the user
@State private var lines: [Line] = []
...
var body: some View {
VStack {
// The canvas
Canvas { context, size in
// 1. Iterate over all the lines drawn and render them on the canvas
for line in lines {
// 2. Create an instance of a path
var path = Path()
// 3. Adding the points of each line collected
path.addLines(line.points)
// 4. Stroke each line with its corresponding color and a line width of 5
context.stroke(path, with: .color(line.color), lineWidth: 5)
}
}
...
}
}
- Iterate over all the lines drawn and collected in
lines
since all of them need to be displayed. - Create a new instances of
Path
for each line stored. - Use the
addLines(_:)
method to add all the collected points of each line to the path just created. - Use the
stroke(_:)
method to create a stroke for each line with its corresponding color and a line width of 5.