Overview

“Drawing” on iOS splits into two paths.

// The lightest way to use CoreGraphics from SwiftUI
Canvas { context, size in
    var path = Path()
    path.move(to: .zero)
    path.addCurve(to: CGPoint(x: size.width, y: size.height),
                  control1: CGPoint(x: size.width, y: 0),
                  control2: CGPoint(x: 0, y: size.height))
    context.stroke(path, with: .color(.purple), lineWidth: 3)
}

The three demos in this module start with high-level PencilKit and gradually descend to low-level CoreGraphics.


1. PencilKit Canvas

What you learn — integrate PKCanvasView with SwiftUI to build a real drawing app in just a few dozen lines.

struct PencilCanvas: UIViewRepresentable {
    @Binding var canvas: PKCanvasView
    let toolPicker = PKToolPicker()

    func makeUIView(context: Context) -> PKCanvasView {
        canvas.drawingPolicy = .anyInput   // accept mouse input in the simulator too
        canvas.tool = PKInkingTool(.pen, color: .black, width: 5)
        toolPicker.setVisible(true, forFirstResponder: canvas)
        toolPicker.addObserver(canvas)
        canvas.becomeFirstResponder()
        return canvas
    }

    func updateUIView(_ uiView: PKCanvasView, context: Context) {}
}

What PencilKit handles automatically:

Exporting is one line:

let image = canvas.drawing.image(from: canvas.bounds, scale: UIScreen.main.scale)

💡 PencilKit’s limits: It’s hard to finely customize the output. If you want full control over tools, colors, and the look of strokes, drop down to the next demo (CoreGraphics).


2. Freehand Drawing

What you learn — build CoreGraphics-based freehand drawing from scratch using SwiftUI’s Canvas and DragGesture.

struct Stroke {
    var color: Color
    var lineWidth: CGFloat
    var points: [CGPoint]
}

@State var strokes: [Stroke] = []
@State var current: Stroke?

Canvas { context, size in
    for stroke in strokes + (current.map { [$0] } ?? []) {
        var path = Path()
        path.addLines(stroke.points)
        context.stroke(path, with: .color(stroke.color), lineWidth: stroke.lineWidth)
    }
}
.gesture(
    DragGesture(minimumDistance: 0)
        .onChanged { value in
            if current == nil {
                current = Stroke(color: selectedColor, lineWidth: width, points: [])
            }
            current?.points.append(value.location)
        }
        .onEnded { _ in
            if let s = current { strokes.append(s) }
            current = nil
        }
)

Key points:


3. Shape Builder

What you learn — interactively draw lines, rectangles, circles, and triangles via touch gestures, and manage stroke and fill colors separately.

enum ShapeKind { case line, rectangle, ellipse, triangle }

struct DrawnShape {
    var kind: ShapeKind
    var rect: CGRect          // bounding rect formed by start and end points
    var stroke: Color
    var fill: Color
}

@State var shapes: [DrawnShape] = []
@State var preview: DrawnShape?

Canvas { context, size in
    for shape in shapes + (preview.map { [$0] } ?? []) {
        let path = path(for: shape)
        context.fill(path, with: .color(shape.fill))
        context.stroke(path, with: .color(shape.stroke), lineWidth: 2)
    }
}
.gesture(
    DragGesture(minimumDistance: 0)
        .onChanged { value in
            let r = CGRect(start: value.startLocation, end: value.location)
            preview = DrawnShape(kind: kind, rect: r, stroke: strokeColor, fill: fillColor)
        }
        .onEnded { _ in
            if let p = preview { shapes.append(p) }
            preview = nil
        }
)

Key takeaways:


Practical Tips

Best Practices

Watch Out