pencilkit-drawing by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill pencilkit-drawing使用 PKCanvasView 捕获 Apple Pencil 和手指输入,使用 PKToolPicker 管理绘图工具,使用 PKDrawing 序列化绘图,并在 SwiftUI 中封装 PencilKit。目标版本为 Swift 6.2 / iOS 26+。
PencilKit 不需要任何授权或 Info.plist 条目。导入 PencilKit 并创建一个 PKCanvasView。
import PencilKit
平台可用性: iOS 13+, iPadOS 13+, Mac Catalyst 13.1+, visionOS 1.0+。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
PKCanvasView 是 UIScrollView 的子类,用于捕获 Apple Pencil 和手指输入并渲染笔画。
import PencilKit
import UIKit
class DrawingViewController: UIViewController, PKCanvasViewDelegate {
let canvasView = PKCanvasView()
override func viewDidLoad() {
super.viewDidLoad()
canvasView.delegate = self
canvasView.drawingPolicy = .anyInput
canvasView.tool = PKInkingTool(.pen, color: .black, width: 5)
canvasView.frame = view.bounds
canvasView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(canvasView)
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
// 绘图已更改 -- 保存或处理
}
}
| 策略 | 行为 |
|---|---|
.default | Apple Pencil 绘图;手指滚动 |
.anyInput | 铅笔和手指均可绘图 |
.pencilOnly | 仅 Apple Pencil 绘图;手指始终滚动 |
canvasView.drawingPolicy = .pencilOnly
// 设置一个大的绘图区域(可滚动)
canvasView.contentSize = CGSize(width: 2000, height: 3000)
// 启用/禁用标尺
canvasView.isRulerActive = true
// 以编程方式设置当前工具
canvasView.tool = PKInkingTool(.pencil, color: .blue, width: 3)
canvasView.tool = PKEraserTool(.vector)
PKToolPicker 显示一个浮动的绘图工具面板。画布会自动采用选定的工具。
class DrawingViewController: UIViewController {
let canvasView = PKCanvasView()
let toolPicker = PKToolPicker()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
}
创建具有特定工具的工具选择器。
let toolPicker = PKToolPicker(toolItems: [
PKToolPickerInkingItem(type: .pen, color: .black),
PKToolPickerInkingItem(type: .pencil, color: .gray),
PKToolPickerInkingItem(type: .marker, color: .yellow),
PKToolPickerEraserItem(type: .vector),
PKToolPickerLassoItem(),
PKToolPickerRulerItem()
])
| 类型 | 描述 |
|---|---|
.pen | 平滑、压感笔 |
.pencil | 带倾斜阴影的纹理铅笔 |
.marker | 半透明荧光笔 |
.monoline | 统一宽度的笔 |
.fountainPen | 可变宽度的书法笔 |
.watercolor | 可混合的水彩画笔 |
.crayon | 纹理蜡笔 |
PKDrawing 是一个值类型(结构体),用于保存所有笔画数据。将其序列化为 Data 以进行持久化存储。
// 保存
func saveDrawing(_ drawing: PKDrawing) throws {
let data = drawing.dataRepresentation()
try data.write(to: fileURL)
}
// 加载
func loadDrawing() throws -> PKDrawing {
let data = try Data(contentsOf: fileURL)
return try PKDrawing(data: data)
}
var drawing1 = PKDrawing()
let drawing2 = PKDrawing()
drawing1.append(drawing2)
// 非可变操作
let combined = drawing1.appending(drawing2)
let scaled = drawing.transformed(using: CGAffineTransform(scaleX: 2, y: 2))
let translated = drawing.transformed(using: CGAffineTransform(translationX: 100, y: 0))
从绘图生成 UIImage。
func exportImage(from drawing: PKDrawing, scale: CGFloat = 2.0) -> UIImage {
drawing.image(from: drawing.bounds, scale: scale)
}
// 导出特定区域
let region = CGRect(x: 0, y: 0, width: 500, height: 500)
let scale = UITraitCollection.current.displayScale
let croppedImage = drawing.image(from: region, scale: scale)
访问单个笔画、其墨水和控制点。
for stroke in drawing.strokes {
let ink = stroke.ink
print("Ink type: \(ink.inkType), color: \(ink.color)")
print("Bounds: \(stroke.renderBounds)")
// 访问路径点
let path = stroke.path
print("Points: \(path.count), created: \(path.creationDate)")
// 沿路径插值
for point in path.interpolatedPoints(by: .distance(10)) {
print("Location: \(point.location), force: \(point.force)")
}
}
let points = [
PKStrokePoint(location: CGPoint(x: 0, y: 0), timeOffset: 0,
size: CGSize(width: 5, height: 5), opacity: 1,
force: 0.5, azimuth: 0, altitude: .pi / 2),
PKStrokePoint(location: CGPoint(x: 100, y: 100), timeOffset: 0.1,
size: CGSize(width: 5, height: 5), opacity: 1,
force: 0.5, azimuth: 0, altitude: .pi / 2)
]
let path = PKStrokePath(controlPoints: points, creationDate: Date())
let stroke = PKStroke(ink: PKInk(.pen, color: .black), path: path,
transform: .identity, mask: nil)
let drawing = PKDrawing(strokes: [stroke])
将 PKCanvasView 包装在 UIViewRepresentable 中以用于 SwiftUI。
import SwiftUI
import PencilKit
struct CanvasView: UIViewRepresentable {
@Binding var drawing: PKDrawing
@Binding var toolPickerVisible: Bool
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.delegate = context.coordinator
canvas.drawingPolicy = .anyInput
canvas.drawing = drawing
return canvas
}
func updateUIView(_ canvas: PKCanvasView, context: Context) {
if canvas.drawing != drawing {
canvas.drawing = drawing
}
let toolPicker = context.coordinator.toolPicker
toolPicker.setVisible(toolPickerVisible, forFirstResponder: canvas)
if toolPickerVisible { canvas.becomeFirstResponder() }
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, PKCanvasViewDelegate {
let parent: CanvasView
let toolPicker = PKToolPicker()
init(_ parent: CanvasView) {
self.parent = parent
super.init()
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
parent.drawing = canvasView.drawing
}
}
}
struct DrawingScreen: View {
@State private var drawing = PKDrawing()
@State private var showToolPicker = true
var body: some View {
CanvasView(drawing: $drawing, toolPickerVisible: $showToolPicker)
.ignoresSafeArea()
}
}
PaperKit (iOS 26+) 扩展了 PencilKit,提供了完整的标记体验,包括形状、文本框、图像、贴纸和放大镜。当您需要不仅仅是自由绘图时,请使用 PaperKit。
| 功能 | PencilKit | PaperKit |
|---|---|---|
| 自由绘图 | 是 | 是 |
| 形状和线条 | 否 | 是 |
| 文本框 | 否 | 是 |
| 图像和贴纸 | 否 | 是 |
| 标记工具栏 | 否 | 是 |
| 数据模型 | PKDrawing | PaperMarkup |
PaperKit 在底层使用 PencilKit —— PaperMarkupViewController 接受 PKTool 作为其 drawingTool 属性,并且 PaperMarkup 可以追加 PKDrawing。有关 PaperKit 模式,请参阅 references/paperkit-integration.md。
工具选择器仅在其关联的响应者成为第一响应者时才会出现。
// 错误:工具选择器永远不会显示
toolPicker.setVisible(true, forFirstResponder: canvasView)
// 正确:同时成为第一响应者
toolPicker.setVisible(true, forFirstResponder: canvasView)
canvasView.becomeFirstResponder()
每个画布一个 PKToolPicker。创建额外的选择器会导致视觉冲突。
// 错误
func viewDidAppear(_ animated: Bool) {
let picker = PKToolPicker() // 每次出现都创建新的选择器
picker.setVisible(true, forFirstResponder: canvasView)
}
// 正确:将选择器存储为属性
let toolPicker = PKToolPicker()
较新的墨水类型在较旧的操作系统版本上会崩溃。如果您需要向后兼容的绘图,请设置 maximumSupportedContentVersion。
// 错误:保存了包含 .watercolor 的绘图,在 iOS 16 上崩溃
canvasView.tool = PKInkingTool(.watercolor, color: .blue)
// 正确:为兼容性限制内容版本
canvasView.maximumSupportedContentVersion = .version2
PKDrawing 数据不是确定性的;相同的视觉绘图可能产生不同的字节。请改用相等运算符。
// 错误
if drawing1.dataRepresentation() == drawing2.dataRepresentation() { }
// 正确
if drawing1 == drawing2 { }
PKCanvasView.drawingPolicy 设置得当(对于以 Pencil 为主的应用使用 .default)PKToolPicker 存储为属性,而不是每次出现都重新创建canvasView.becomeFirstResponder() 以显示工具选择器dataRepresentation() 序列化绘图并通过 PKDrawing(data:) 加载canvasViewDrawingDidChange 委托方法来跟踪更改maximumSupportedContentVersiondrawing != binding 来避免无限更新循环.zero)references/pencilkit-patterns.mdreferences/paperkit-integration.md每周安装量
331
代码仓库
GitHub 星标
269
首次出现
2026 年 3 月 8 日
安全审计
安装于
codex328
opencode325
github-copilot325
amp325
cline325
kimi-cli325
Capture Apple Pencil and finger input using PKCanvasView, manage drawing tools with PKToolPicker, serialize drawings with PKDrawing, and wrap PencilKit in SwiftUI. Targets Swift 6.2 / iOS 26+.
PencilKit requires no entitlements or Info.plist entries. Import PencilKit and create a PKCanvasView.
import PencilKit
Platform availability: iOS 13+, iPadOS 13+, Mac Catalyst 13.1+, visionOS 1.0+.
PKCanvasView is a UIScrollView subclass that captures Apple Pencil and finger input and renders strokes.
import PencilKit
import UIKit
class DrawingViewController: UIViewController, PKCanvasViewDelegate {
let canvasView = PKCanvasView()
override func viewDidLoad() {
super.viewDidLoad()
canvasView.delegate = self
canvasView.drawingPolicy = .anyInput
canvasView.tool = PKInkingTool(.pen, color: .black, width: 5)
canvasView.frame = view.bounds
canvasView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(canvasView)
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
// Drawing changed -- save or process
}
}
| Policy | Behavior |
|---|---|
.default | Apple Pencil draws; finger scrolls |
.anyInput | Both pencil and finger draw |
.pencilOnly | Only Apple Pencil draws; finger always scrolls |
canvasView.drawingPolicy = .pencilOnly
// Set a large drawing area (scrollable)
canvasView.contentSize = CGSize(width: 2000, height: 3000)
// Enable/disable the ruler
canvasView.isRulerActive = true
// Set the current tool programmatically
canvasView.tool = PKInkingTool(.pencil, color: .blue, width: 3)
canvasView.tool = PKEraserTool(.vector)
PKToolPicker displays a floating palette of drawing tools. The canvas automatically adopts the selected tool.
class DrawingViewController: UIViewController {
let canvasView = PKCanvasView()
let toolPicker = PKToolPicker()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
}
Create a tool picker with specific tools.
let toolPicker = PKToolPicker(toolItems: [
PKToolPickerInkingItem(type: .pen, color: .black),
PKToolPickerInkingItem(type: .pencil, color: .gray),
PKToolPickerInkingItem(type: .marker, color: .yellow),
PKToolPickerEraserItem(type: .vector),
PKToolPickerLassoItem(),
PKToolPickerRulerItem()
])
| Type | Description |
|---|---|
.pen | Smooth, pressure-sensitive pen |
.pencil | Textured pencil with tilt shading |
.marker | Semi-transparent highlighter |
.monoline | Uniform-width pen |
.fountainPen | Variable-width calligraphy pen |
.watercolor | Blendable watercolor brush |
PKDrawing is a value type (struct) that holds all stroke data. Serialize it to Data for persistence.
// Save
func saveDrawing(_ drawing: PKDrawing) throws {
let data = drawing.dataRepresentation()
try data.write(to: fileURL)
}
// Load
func loadDrawing() throws -> PKDrawing {
let data = try Data(contentsOf: fileURL)
return try PKDrawing(data: data)
}
var drawing1 = PKDrawing()
let drawing2 = PKDrawing()
drawing1.append(drawing2)
// Non-mutating
let combined = drawing1.appending(drawing2)
let scaled = drawing.transformed(using: CGAffineTransform(scaleX: 2, y: 2))
let translated = drawing.transformed(using: CGAffineTransform(translationX: 100, y: 0))
Generate a UIImage from a drawing.
func exportImage(from drawing: PKDrawing, scale: CGFloat = 2.0) -> UIImage {
drawing.image(from: drawing.bounds, scale: scale)
}
// Export a specific region
let region = CGRect(x: 0, y: 0, width: 500, height: 500)
let scale = UITraitCollection.current.displayScale
let croppedImage = drawing.image(from: region, scale: scale)
Access individual strokes, their ink, and control points.
for stroke in drawing.strokes {
let ink = stroke.ink
print("Ink type: \(ink.inkType), color: \(ink.color)")
print("Bounds: \(stroke.renderBounds)")
// Access path points
let path = stroke.path
print("Points: \(path.count), created: \(path.creationDate)")
// Interpolate along the path
for point in path.interpolatedPoints(by: .distance(10)) {
print("Location: \(point.location), force: \(point.force)")
}
}
let points = [
PKStrokePoint(location: CGPoint(x: 0, y: 0), timeOffset: 0,
size: CGSize(width: 5, height: 5), opacity: 1,
force: 0.5, azimuth: 0, altitude: .pi / 2),
PKStrokePoint(location: CGPoint(x: 100, y: 100), timeOffset: 0.1,
size: CGSize(width: 5, height: 5), opacity: 1,
force: 0.5, azimuth: 0, altitude: .pi / 2)
]
let path = PKStrokePath(controlPoints: points, creationDate: Date())
let stroke = PKStroke(ink: PKInk(.pen, color: .black), path: path,
transform: .identity, mask: nil)
let drawing = PKDrawing(strokes: [stroke])
Wrap PKCanvasView in a UIViewRepresentable for SwiftUI.
import SwiftUI
import PencilKit
struct CanvasView: UIViewRepresentable {
@Binding var drawing: PKDrawing
@Binding var toolPickerVisible: Bool
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.delegate = context.coordinator
canvas.drawingPolicy = .anyInput
canvas.drawing = drawing
return canvas
}
func updateUIView(_ canvas: PKCanvasView, context: Context) {
if canvas.drawing != drawing {
canvas.drawing = drawing
}
let toolPicker = context.coordinator.toolPicker
toolPicker.setVisible(toolPickerVisible, forFirstResponder: canvas)
if toolPickerVisible { canvas.becomeFirstResponder() }
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, PKCanvasViewDelegate {
let parent: CanvasView
let toolPicker = PKToolPicker()
init(_ parent: CanvasView) {
self.parent = parent
super.init()
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
parent.drawing = canvasView.drawing
}
}
}
struct DrawingScreen: View {
@State private var drawing = PKDrawing()
@State private var showToolPicker = true
var body: some View {
CanvasView(drawing: $drawing, toolPickerVisible: $showToolPicker)
.ignoresSafeArea()
}
}
PaperKit (iOS 26+) extends PencilKit with a complete markup experience including shapes, text boxes, images, stickers, and loupes. Use PaperKit when you need more than freeform drawing.
| Capability | PencilKit | PaperKit |
|---|---|---|
| Freeform drawing | Yes | Yes |
| Shapes & lines | No | Yes |
| Text boxes | No | Yes |
| Images & stickers | No | Yes |
| Markup toolbar | No | Yes |
| Data model | PKDrawing | PaperMarkup |
PaperKit uses PencilKit under the hood -- PaperMarkupViewController accepts PKTool for its drawingTool property and PaperMarkup can append a PKDrawing. See references/paperkit-integration.md for PaperKit patterns.
The tool picker only appears when its associated responder is first responder.
// WRONG: Tool picker never shows
toolPicker.setVisible(true, forFirstResponder: canvasView)
// CORRECT: Also become first responder
toolPicker.setVisible(true, forFirstResponder: canvasView)
canvasView.becomeFirstResponder()
One PKToolPicker per canvas. Creating extras causes visual conflicts.
// WRONG
func viewDidAppear(_ animated: Bool) {
let picker = PKToolPicker() // New picker every appearance
picker.setVisible(true, forFirstResponder: canvasView)
}
// CORRECT: Store picker as a property
let toolPicker = PKToolPicker()
Newer ink types crash on older OS versions. Set maximumSupportedContentVersion if you need backward-compatible drawings.
// WRONG: Saves a drawing with .watercolor, crashes on iOS 16
canvasView.tool = PKInkingTool(.watercolor, color: .blue)
// CORRECT: Limit content version for compatibility
canvasView.maximumSupportedContentVersion = .version2
PKDrawing data is not deterministic; the same visual drawing can produce different bytes. Use equality operators instead.
// WRONG
if drawing1.dataRepresentation() == drawing2.dataRepresentation() { }
// CORRECT
if drawing1 == drawing2 { }
PKCanvasView.drawingPolicy set appropriately (.default for Pencil-primary apps)PKToolPicker stored as a property, not recreated each appearancecanvasView.becomeFirstResponder() called to show the tool pickerdataRepresentation() and loaded via PKDrawing(data:)canvasViewDrawingDidChange delegate method used to track changesmaximumSupportedContentVersion set if backward compatibility neededdrawing != bindingreferences/pencilkit-patterns.mdreferences/paperkit-integration.mdWeekly Installs
331
Repository
GitHub Stars
269
First Seen
Mar 8, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex328
opencode325
github-copilot325
amp325
cline325
kimi-cli325
OpenAI Assistants API v2 使用指南与迁移方案 - 2026年弃用前必看
313 周安装
MCP CLI 脚本开发指南:为Claude Code构建高效本地工具与自动化脚本
313 周安装
Playwright MCP 开发指南:如何为微软 Playwright 添加 MCP 工具和 CLI 命令
313 周安装
Shopify开发专家 | 电商平台定制、API集成、主题开发与无头电商解决方案
313 周安装
Sentry SDK 设置指南:跨平台错误监控、性能追踪与会话回放集成
313 周安装
LLM安全指南:OWASP LLM十大安全风险2025防护规则与最佳实践
313 周安装
.crayon | Textured crayon |
.zero bounds)