๊ฐœ์š”

iOS์—์„œ โ€œ๊ทธ๋ฆฌ๊ธฐโ€๋Š” ๋‘ ๊ฐˆ๋ž˜์ž…๋‹ˆ๋‹ค.

// SwiftUI์—์„œ CoreGraphics๋ฅผ ๊ฐ€์žฅ ๊ฐ€๋ณ๊ฒŒ ์“ฐ๋Š” ๋ฐฉ๋ฒ•
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)
}

์ด ๋ชจ๋“ˆ์˜ 3๊ฐœ ๋ฐ๋ชจ๋Š” ๊ณ ์ˆ˜์ค€ PencilKit์œผ๋กœ ์‹œ์ž‘ํ•ด ์ ์ฐจ ์ €์ˆ˜์ค€ CoreGraphics๋กœ ๋‚ด๋ ค๊ฐ€๋ฉฐ ๋“œ๋กœ์ž‰์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.


1. PencilKit Canvas

๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” PKCanvasView๋ฅผ SwiftUI์— ํ†ตํ•ฉํ•ด ์ˆ˜์‹ญ ์ค„๋กœ ๋ณธ๊ฒฉ ๋“œ๋กœ์ž‰ ์•ฑ์„ ๋งŒ๋“œ๋Š” ํŒจํ„ด.

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

    func makeUIView(context: Context) -> PKCanvasView {
        canvas.drawingPolicy = .anyInput   // ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ๋„ ๋งˆ์šฐ์Šค ์ž…๋ ฅ ํ—ˆ์šฉ
        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) {}
}

PencilKit์ด ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ๊ฒƒ:

๋‚ด๋ณด๋‚ด๊ธฐ๋Š” ํ•œ ์ค„:

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

๐Ÿ’ก PencilKit์˜ ํ•œ๊ณ„: ๊ฒฐ๊ณผ๋ฌผ์— ๋Œ€ํ•œ ์„ธ๋ฐ€ํ•œ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์€ ์ œ์•ฝ์ด ํฝ๋‹ˆ๋‹ค. ๋„๊ตฌยท์ƒ‰์ƒยท๊ฒฐ๊ณผ๋ฌผ์˜ ๋ชจ์–‘์„ ์ž์œ ๋กญ๊ฒŒ ์ œ์–ดํ•˜๊ณ  ์‹ถ์œผ๋ฉด ๋‹ค์Œ ๋ฐ๋ชจ(CoreGraphics)๋กœ ๋‚ด๋ ค๊ฐ€์„ธ์š”.


2. Freehand Drawing

๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” SwiftUI Canvas์™€ DragGesture๋กœ CoreGraphics ๊ธฐ๋ฐ˜ ์ž์œ  ๋“œ๋กœ์ž‰์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋งŒ๋“œ๋Š” ํŒจํ„ด.

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
        }
)

ํ•ต์‹ฌ ํฌ์ธํŠธ:


3. Shape Builder

๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” ํ„ฐ์น˜ ์ œ์Šค์ฒ˜๋กœ ์„ ยท์‚ฌ๊ฐํ˜•ยท์›ยท์‚ผ๊ฐํ˜•์„ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•˜๊ฒŒ ๊ทธ๋ฆฌ๊ณ , stroke์™€ fill ์ƒ‰์ƒ์„ ๋ถ„๋ฆฌํ•ด ๋‹ค๋ฃจ๋Š” ํŒจํ„ด.

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

struct DrawnShape {
    var kind: ShapeKind
    var rect: CGRect          // ์‹œ์ž‘์ -๋์ ์ด ๋งŒ๋“œ๋Š” ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜•
    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
        }
)

์—ฌ๊ธฐ์„œ ๋ฐฐ์šฐ๋Š” ํ•ต์‹ฌ:


์‹ค์ „ ํŒ

Best Practices

์ฃผ์˜ ์‚ฌํ•ญ