✏️ PencilKit

손글씨와 드로잉을 위한 강력한 캔버스

iOS 13+Apple Pencil 최적화

✨ PencilKit이란?

PencilKit은 Apple의 드로잉 프레임워크로, Apple Pencil과 완벽하게 통합되어 저지연 손글씨 및 그림 그리기 경험을 제공합니다. 펜, 마커, 연필 도구, 지우개, 눈금자 등을 기본 제공하며, 손글씨 인식과 데이터 저장을 지원합니다.

💡 핵심 기능: 저지연 드로잉 · Apple Pencil 지원 · 다양한 도구 · 필기 인식 · 실행 취소/재실행 · 데이터 저장/공유 · 손가락/터치 지원 · 다크 모드

🎯 1. 기본 캔버스 설정

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. 도구 설정

펜, 마커, 연필 등 다양한 드로잉 도구를 설정합니다.

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 — 저장/불러오기
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 — 손글씨 인식
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. 서명 캡처

전자 서명을 캡처하는 기능을 구현합니다.

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

📱 종합 예제

DrawingAppView.swift — 드로잉 앱
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 가이드라인

🎯 실전 활용

📚 더 알아보기

⚡️ 성능 팁: drawingPolicy.pencilOnly로 설정하면 손가락 터치를 무시하고 Apple Pencil만 인식합니다. 정교한 드로잉이 필요할 때 유용합니다.