๐ŸŒ KO

โœ๏ธ PencilKit

โญ Difficulty: โญโญ โฑ๏ธ Est. Time: 1-2h ๐Ÿ“‚ Graphics & Media

์†๊ธ€์”จ์™€ ๋“œ๋กœ์ž‰์„ ์œ„ํ•œ ๊ฐ•๋ ฅํ•œ ์บ”๋ฒ„์Šค

iOS 13+Apple Pencil ์ตœ์ ํ™”

โœจ PencilKit is?

PencilKit is Apple's drawing framework, perfectly integrated with Apple Pencil to deliver a low-latency handwriting and sketching experience. It provides pen, marker, pencil tools, eraser, and ruler out of the box, with support for handwriting recognition and data persistence.

๐Ÿ’ก Key Features: Low-Latency Drawing ยท Apple Pencil Support ยท Various Tools ยท Handwriting Recognition ยท Undo/Redo ยท Data Save/Share ยท Finger/Touch Support ยท Dark Mode

๐ŸŽฏ 1. ๊ธฐ๋ณธ ์บ”๋ฒ„์Šค ์„ค์ •

Create a drawing canvas using PKCanvasView.

DrawingView.swift โ€” ๊ธฐ๋ณธ ์บ”๋ฒ„์Šค
import SwiftUI
import PencilKit

struct DrawingView: View {
    @State private var canvasView = PKCanvasView()
    @State private var toolPicker = PKToolPicker()

    var body: some View {
        CanvasView(canvasView: $canvasView, toolPicker: toolPicker)
            .onAppear {
                // ํˆด ํ”ผ์ปค ํ‘œ์‹œ
                toolPicker.setVisible(true, forFirstResponder: canvasView)
                toolPicker.addObserver(canvasView)
                canvasView.becomeFirstResponder()
            }
    }
}

struct CanvasView: UIViewRepresentable {
    @Binding var canvasView: PKCanvasView
    let toolPicker: PKToolPicker

    func makeUIView(context: Context) -> PKCanvasView {
        canvasView.drawingPolicy = .anyInput // Apple Pencil + ์†๊ฐ€๋ฝ
        canvasView.delegate = context.coordinator
        return canvasView
    }

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

    func makeCoordinator() -> Coordinator {
        Coordinator(canvasView: $canvasView)
    }

    class Coordinator: NSObject, PKCanvasViewDelegate {
        @Binding var canvasView: PKCanvasView

        init(canvasView: Binding<PKCanvasView>) {
            _canvasView = canvasView
        }

        // ๋“œ๋กœ์ž‰ ๋ณ€๊ฒฝ ์‹œ ํ˜ธ์ถœ
        func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
            print("๋“œ๋กœ์ž‰ ๋ณ€๊ฒฝ๋จ")
        }
    }
}

๐Ÿ–Š๏ธ 2. ๋„๊ตฌ ์„ค์ •

Configure various drawing tools like pen, marker, pencil.

DrawingTools.swift โ€” ๋„๊ตฌ ์„ค์ •
import PencilKit

class DrawingToolManager {
    // 1. ํŽœ ๋„๊ตฌ
    func createPenTool(color: UIColor = .black, width: CGFloat = 5) -> PKInkingTool {
        let ink = PKInkingTool(.pen, color: color, width: width)
        return ink
    }

    // 2. ๋งˆ์ปค ๋„๊ตฌ
    func createMarkerTool(color: UIColor = .yellow, width: CGFloat = 20) -> PKInkingTool {
        let ink = PKInkingTool(.marker, color: color, width: width)
        return ink
    }

    // 3. ์—ฐํ•„ ๋„๊ตฌ
    func createPencilTool(color: UIColor = .darkGray, width: CGFloat = 3) -> PKInkingTool {
        let ink = PKInkingTool(.pencil, color: color, width: width)
        return ink
    }

    // 4. ์ง€์šฐ๊ฐœ ๋„๊ตฌ
    func createEraserTool(eraserType: PKEraserTool.EraserType = .vector) -> PKEraserTool {
        // .vector = ํš ๋‹จ์œ„ ์ง€์šฐ๊ธฐ
        // .bitmap = ํ”ฝ์…€ ๋‹จ์œ„ ์ง€์šฐ๊ธฐ
        return PKEraserTool(eraserType)
    }

    // 5. ๋ˆˆ๊ธˆ์ž ๋„๊ตฌ
    func setupRuler(for canvasView: PKCanvasView) {
        canvasView.isRulerActive = true
    }

    // ๋„๊ตฌ ์ ์šฉ
    func applyTool(_ tool: PKTool, to canvasView: PKCanvasView) {
        canvasView.tool = tool
    }
}

๐Ÿ’พ 3. ๋“œ๋กœ์ž‰ ์ €์žฅ ๋ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ

๋“œ๋กœ์ž‰ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถˆ๋Ÿฌ์˜ต.

DrawingStorage.swift โ€” Save/Load
import PencilKit

class DrawingStorageManager {
    // ๋“œ๋กœ์ž‰ ์ €์žฅ
    func saveDrawing(_ drawing: PKDrawing, to url: URL) throws {
        // PKDrawing์„ Data๋กœ ๋ณ€ํ™˜
        let data = drawing.dataRepresentation()

        // ํŒŒ์ผ๋กœ ์ €์žฅ
        try data.write(to: url)
    }

    // ๋“œ๋กœ์ž‰ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
    func loadDrawing(from url: URL) throws -> PKDrawing {
        // ํŒŒ์ผ์—์„œ Data ์ฝ๊ธฐ
        let data = try Data(contentsOf: url)

        // Data๋ฅผ PKDrawing์œผ๋กœ ๋ณ€ํ™˜
        let drawing = try PKDrawing(data: data)
        return drawing
    }

    // ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜
    func convertToImage(_ drawing: PKDrawing, size: CGSize) -> UIImage {
        let image = drawing.image(from: drawing.bounds, scale: UIScreen.main.scale)
        return image
    }

    // UserDefaults์— ์ €์žฅ
    func saveToUserDefaults(_ drawing: PKDrawing, key: String) {
        let data = drawing.dataRepresentation()
        UserDefaults.standard.set(data, forKey: key)
    }

    // UserDefaults์—์„œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
    func loadFromUserDefaults(key: String) -> PKDrawing? {
        guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
        return try? PKDrawing(data: data)
    }
}

โœ๏ธ 4. ์†๊ธ€์”จ ์ธ์‹

์†์œผ๋กœ ์“ด ํ…์ŠคํŠธ๋ฅผ ์ธ์‹.

HandwritingRecognition.swift โ€” Handwriting Recognition
import PencilKit
import Vision

@Observable
class HandwritingRecognizer {
    var recognizedText: String = ""

    // ๋“œ๋กœ์ž‰์—์„œ ํ…์ŠคํŠธ ์ธ์‹
    func recognizeText(from drawing: PKDrawing) async {
        // ๋“œ๋กœ์ž‰์„ ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜
        let image = drawing.image(from: drawing.bounds, scale: 2.0)

        guard let cgImage = image.cgImage else { return }

        // Vision ํ…์ŠคํŠธ ์ธ์‹
        let request = VNRecognizeTextRequest { [weak self] request, error in
            guard let observations = request.results as? [VNRecognizedTextObservation] else {
                return
            }

            let recognizedStrings = observations.compactMap { observation in
                observation.topCandidates(1).first?.string
            }

            Task { @MainActor in
                self?.recognizedText = recognizedStrings.joined(separator: "\n")
            }
        }

        // ์†๊ธ€์”จ ์ธ์‹ ํ™œ์„ฑํ™”
        request.recognitionLevel = .accurate
        request.recognitionLanguages = ["en-US", "ko-KR"]

        let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
        try? handler.perform([request])
    }
}

๐Ÿ“ 5. ์„œ๋ช… ์บก์ฒ˜

์ „์ž ์„œ๋ช…์„ ์บก์ฒ˜ํ•˜๋Š” ๊ธฐ๋Šฅ implementation.

SignatureView.swift โ€” ์„œ๋ช… ์บก์ฒ˜
import SwiftUI
import PencilKit

struct SignatureView: View {
    @State private var canvasView = PKCanvasView()
    @State private var signatureImage: UIImage?
    @State private var showSignaturePad = false

    var body: some View {
        VStack {
            if let image = signatureImage {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(height: 150)
                    .border(Color.gray)
                    .padding()
            } else {
                Text("์„œ๋ช… ์—†์Œ")
                    .foregroundStyle(.secondary)
            }

            Button("์„œ๋ช…ํ•˜๊ธฐ") {
                showSignaturePad = true
            }
            .buttonStyle(.borderedProminent)
        }
        .sheet(isPresented: $showSignaturePad) {
            SignaturePadView(
                canvasView: $canvasView,
                onSave: { drawing in
                    signatureImage = drawing.image(
                        from: drawing.bounds,
                        scale: UIScreen.main.scale
                    )
                    showSignaturePad = false
                }
            )
        }
    }
}

struct SignaturePadView: View {
    @Binding var canvasView: PKCanvasView
    let onSave: (PKDrawing) -> Void
    @Environment(\.dismiss) var dismiss

    var body: some View {
        NavigationStack {
            VStack {
                Text("์•„๋ž˜์— ์„œ๋ช…ํ•ด์ฃผ์„ธ์š”")
                    .font(.headline)
                    .padding()

                CanvasView(canvasView: $canvasView, toolPicker: PKToolPicker())
                    .frame(height: 300)
                    .border(Color.gray)
                    .padding()
            }
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("์ทจ์†Œ") {
                        dismiss()
                    }
                }

                ToolbarItem(placement: .confirmationAction) {
                    Button("์ €์žฅ") {
                        onSave(canvasView.drawing)
                    }
                }

                ToolbarItem(placement: .bottomBar) {
                    Button("์ง€์šฐ๊ธฐ") {
                        canvasView.drawing = PKDrawing()
                    }
                }
            }
        }
    }
}

๐Ÿ“ฑ Complete Example

DrawingAppView.swift โ€” App
import SwiftUI
import PencilKit

struct DrawingAppView: View {
    @State private var canvasView = PKCanvasView()
    @State private var toolPicker = PKToolPicker()
    @State private var showSaveAlert = false

    let storageManager = DrawingStorageManager()

    var body: some View {
        NavigationStack {
            CanvasView(canvasView: $canvasView, toolPicker: toolPicker)
                .navigationTitle("๋“œ๋กœ์ž‰")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItemGroup(placement: .topBarTrailing) {
                        Button {
                            // ์ด๋ฏธ์ง€๋กœ ์ €์žฅ
                            let image = canvasView.drawing.image(
                                from: canvasView.drawing.bounds,
                                scale: UIScreen.main.scale
                            )
                            UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
                            showSaveAlert = true
                        } label: {
                            Image(systemName: "square.and.arrow.down")
                        }

                        Button {
                            canvasView.drawing = PKDrawing()
                        } label: {
                            Image(systemName: "trash")
                        }
                    }

                    ToolbarItemGroup(placement: .bottomBar) {
                        Button {
                            canvasView.undoManager?.undo()
                        } label: {
                            Image(systemName: "arrow.uturn.backward")
                        }
                        .disabled(canvasView.undoManager?.canUndo == false)

                        Spacer()

                        Button {
                            canvasView.undoManager?.redo()
                        } label: {
                            Image(systemName: "arrow.uturn.forward")
                        }
                        .disabled(canvasView.undoManager?.canRedo == false)
                    }
                }
                .alert("์ €์žฅ ์™„๋ฃŒ", isPresented: $showSaveAlert) {
                    Button("ํ™•์ธ", role: .cancel) {}
                } message: {
                    Text("๋“œ๋กœ์ž‰์ด ์‚ฌ์ง„์ฒฉ์— ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค")
                }
                .onAppear {
                    toolPicker.setVisible(true, forFirstResponder: canvasView)
                    toolPicker.addObserver(canvasView)
                    canvasView.becomeFirstResponder()
                }
        }
    }
}

๐Ÿ’ก HIG Guidelines

๐ŸŽฏ Practical Usage

๐Ÿ“š Learn More

โšก๏ธ Performance Tips: drawingPolicy๋ฅผ .pencilOnly to ignore finger touch and recognize only Apple Pencil. Useful for precise drawing.

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐Ÿ’ป Sample Code ๐ŸŽฌ WWDC Sessions