✏️ 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 가이드라인
- Apple Pencil: Apple Pencil 사용 시 최적화된 경험 제공
- 도구 선택: 명확한 도구 선택 UI 제공
- 실행 취소: 쉬운 실행 취소/재실행 버튼
- 저장: 자동 저장 또는 명확한 저장 버튼
- 권한: Info.plist에
NSPhotoLibraryAddUsageDescription추가 (사진 저장 시)
🎯 실전 활용
- 노트 앱: 손글씨 필기 및 스케치
- 화이트보드: 협업 드로잉
- 그림 앱: 디지털 드로잉
- 서명: 전자 서명 캡처
- 주석: 문서/이미지 주석
📚 더 알아보기
⚡️ 성능 팁:
drawingPolicy를 .pencilOnly로 설정하면 손가락 터치를 무시하고 Apple Pencil만 인식합니다. 정교한 드로잉이 필요할 때 유용합니다.