Drawing App in SwiftUI 3 using Canvas

DevTechie Inc
Jun 17, 2022

In this article, we will build a drawing app in SwiftUI 3 using newly introduced Canvas view. Final output will look like this:



We will start with a simple data structure to store points of line that needs to be drawn on screen. This data structure will also store color for the line.

struct Line {
    var points: [CGPoint]
    var color: Color
}
We will need two state variables, one to keep track of all the lines user has drawn, another to store the selected color for drawing.

struct CanvasDrawingExample: View {
    
    @State private var lines: [Line] = []
    @State private var selectedColor = Color.orange
    
    var body: some View {
        VStack {
           
        }
    }
    
}
Next we will need color picker buttons (you can include color picker instead if you like but I will simply create a few color buttons). We will create a ViewBuilder function to get the button created for each color:

@ViewBuilder
    func colorButton(color: Color) -> some View {
        Button {
            selectedColor = color
        } label: {
            Image(systemName: "circle.fill")
                .font(.largeTitle)
                .foregroundColor(color)
                .mask {
                    Image(systemName: "pencil.tip")
                        .font(.largeTitle)
                }
        }
    }
We also need a button to clear the canvas, this will be done by simply setting the lines array to an empty one.

@ViewBuilder
func clearButton() -> some View {
    Button {
        lines = []
    } label: {
        Image(systemName: "pencil.tip.crop.circle.badge.minus")
            .font(.largeTitle)
            .foregroundColor(.gray)
    }
}
We will add these buttons inside a HStack:

struct CanvasDrawingExample: View {
    
    @State private var lines: [Line] = []
    @State private var selectedColor = Color.orange
    
    var body: some View {
        VStack {
           HStack {
                ForEach([Color.green, .orange, .blue, .red, .pink, .black, .purple], id: \.self) { color in
                    colorButton(color: color)
                }
                clearButton()
            }
        }
    }
    
    @ViewBuilder
    func colorButton(color: Color) -> some View {
        Button {
            selectedColor = color
        } label: {
            Image(systemName: "circle.fill")
                .font(.largeTitle)
                .foregroundColor(color)
                .mask {
                    Image(systemName: "pencil.tip")
                        .font(.largeTitle)
                }
        }
    }
    
    @ViewBuilder
    func clearButton() -> some View {
        Button {
            lines = []
        } label: {
            Image(systemName: "pencil.tip.crop.circle.badge.minus")
                .font(.largeTitle)
                .foregroundColor(.gray)
        }
    }
}
Now we will add Canvas view and draw path inside the canvas for each line stored in lines array:

struct CanvasDrawingExample: View {
    
    @State private var lines: [Line] = []
    @State private var selectedColor = Color.orange
    
    var body: some View {
        VStack {
            HStack {
                ForEach([Color.green, .orange, .blue, .red, .pink, .black, .purple], id: \.self) { color in
                    colorButton(color: color)
                }
                clearButton()
            }
            
            Canvas {ctx, size in
                for line in lines {
                    var path = Path()
                    path.addLines(line.points)
                    
                    ctx.stroke(path, with: .color(line.color), style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
                }
            }
        }
    }
    
    @ViewBuilder
    func colorButton(color: Color) -> some View {
        Button {
            selectedColor = color
        } label: {
            Image(systemName: "circle.fill")
                .font(.largeTitle)
                .foregroundColor(color)
                .mask {
                    Image(systemName: "pencil.tip")
                        .font(.largeTitle)
                }
        }
    }
    
    @ViewBuilder
    func clearButton() -> some View {
        Button {
            lines = []
        } label: {
            Image(systemName: "pencil.tip.crop.circle.badge.minus")
                .font(.largeTitle)
                .foregroundColor(.gray)
        }
    }
}
We will use DragGesture to detect user’s touch locations on canvas and store those points inside the lines array. These lines will eventually will be drawn on screen inside the Canvas view:

struct CanvasDrawingExample: View {
    
    @State private var lines: [Line] = []
    @State private var selectedColor = Color.orange
    
    var body: some View {
        VStack {
            HStack {
                ForEach([Color.green, .orange, .blue, .red, .pink, .black, .purple], id: \.self) { color in
                    colorButton(color: color)
                }
                clearButton()
            }
            
            Canvas {ctx, size in
                for line in lines {
                    var path = Path()
                    path.addLines(line.points)
                    
                    ctx.stroke(path, with: .color(line.color), style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
                }
            }
            .gesture(
                DragGesture(minimumDistance: 0, coordinateSpace: .local)
                    .onChanged({ value in
                        let position = value.location
                        
                        if value.translation == .zero {
                            lines.append(Line(points: [position], color: selectedColor))
                        } else {
                            guard let lastIdx = lines.indices.last else {
                                return
                            }
                            
                            lines[lastIdx].points.append(position)
                        }
                    })
            )
        }
    }
    
    @ViewBuilder
    func colorButton(color: Color) -> some View {
        Button {
            selectedColor = color
        } label: {
            Image(systemName: "circle.fill")
                .font(.largeTitle)
                .foregroundColor(color)
                .mask {
                    Image(systemName: "pencil.tip")
                        .font(.largeTitle)
                }
        }
    }
    
    @ViewBuilder
    func clearButton() -> some View {
        Button {
            lines = []
        } label: {
            Image(systemName: "pencil.tip.crop.circle.badge.minus")
                .font(.largeTitle)
                .foregroundColor(.gray)
        }
    }
}
Build and run the app:

Final code:

import SwiftUI
struct Line {
    var points: [CGPoint]
    var color: Color
}
struct CanvasDrawingExample: View {
    
    @State private var lines: [Line] = []
    @State private var selectedColor = Color.orange
    
    var body: some View {
        VStack {
            HStack {
                ForEach([Color.green, .orange, .blue, .red, .pink, .black, .purple], id: \.self) { color in
                    colorButton(color: color)
                }
                clearButton()
            }
            
            Canvas {ctx, size in
                for line in lines {
                    var path = Path()
                    path.addLines(line.points)
                    
                    ctx.stroke(path, with: .color(line.color), style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
                }
            }
            .gesture(
                DragGesture(minimumDistance: 0, coordinateSpace: .local)
                    .onChanged({ value in
                        let position = value.location
                        
                        if value.translation == .zero {
                            lines.append(Line(points: [position], color: selectedColor))
                        } else {
                            guard let lastIdx = lines.indices.last else {
                                return
                            }
                            
                            lines[lastIdx].points.append(position)
                        }
                    })
            )
        }
    }
    
    @ViewBuilder
    func colorButton(color: Color) -> some View {
        Button {
            selectedColor = color
        } label: {
            Image(systemName: "circle.fill")
                .font(.largeTitle)
                .foregroundColor(color)
                .mask {
                    Image(systemName: "pencil.tip")
                        .font(.largeTitle)
                }
        }
    }
    
    @ViewBuilder
    func clearButton() -> some View {
        Button {
            lines = []
        } label: {
            Image(systemName: "pencil.tip.crop.circle.badge.minus")
                .font(.largeTitle)
                .foregroundColor(.gray)
        }
    }
}
struct CanvasDrawingExample_Previews: PreviewProvider {
    static var previews: some View {
        CanvasDrawingExample()
    }
}


With that we have reached the end of this article. Thank you once again for reading. Subscribe our weekly newsletter at https://www.devtechie.com