mcpField
P
paperkit

paperkit

github.com/dpearson2699文档处理1.2k3.1k 次安装13 周前更新
代码仓库

关于 paperkit

面向 Apple 平台开发者的 PaperKit 指南,介绍如何构建结合 PencilKit 绘图与结构化标记的画布。涵盖标记数据保存、功能集配置及界面集成;该框架自 Apple 26 系统版本起提供,API 仍可能变化。

主要功能

  1. 01绘制并编辑结构化标记
  2. 02插入文本、形状和图像
  3. 03配置支持的功能集
  4. 04保存和加载标记数据
  5. 05与 PencilKit 和 SwiftUI 集成

适用场景

  1. 01在文档页面上绘制和添加标注
  2. 02用文本框、箭头和标签进行评审
  3. 03保存画布内容并在之后恢复

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill paperkit

安装后在 Claude Code 中输入/paperkit确认已加载。

附带文件evals/evals.jsonreferences/paperkit-patterns.md

详细介绍

PaperKit

对 Beta 版本敏感。 PaperKit 是 iOS/iPadOS 26、macOS 26 和 visionOS 26 中的新功能。API 接口可能会变更。发布前请对照 Apple 当前的文档核实详情。

PaperKit 将 PencilKit 绘图与形状、文本、图像和线条等结构化标记元素结合在由 PaperMarkupViewController 管理的画布中。

目录

工作流

  1. 在构建 UI 之前,选择文档边界、受支持的 FeatureSet 和持久化版本。
  2. 创建 PaperMarkup,嵌入 PaperMarkupViewController,并在视图的生命周期内保持控制器、工具选取器和插入控制器处于活动状态。
  3. 使用适合平台的插入界面,并确保 PencilKit 绘图始终位于 PaperKit 文档边界内。
  4. 在主线程之外保存,保留缩略图以处理不兼容未来版本的内容,并使用相同的功能集测试往返加载。
  5. 失败时,恢复原始文档字节,修正功能集/版本/控制器不匹配的问题,然后重新执行编辑、保存、重新启动、加载、缩略图回退和撤销检查。

请加载 references/paperkit-patterns.md,了解完整的平台设置、工具选取器连接、持久化、缩略图、自定义功能集、程序化构建和迁移。

设置

PaperKit 无需任何授权或特殊的 Info.plist 条目。

import PaperKit

平台可用性: iOS 26.0+、iPadOS 26.0+、Mac Catalyst 26.0+、macOS 26.0+、visionOS 26.0+。

三个核心组件:

组件作用
PaperMarkupViewController用于创建和显示标记与绘图的交互式画布
PaperMarkup用于序列化所有标记元素和 PencilKit 绘图的数据模型
MarkupEditViewController / MarkupToolbarViewController用于添加标记元素的插入 UI

PaperMarkupViewController

用于交互式标记的主要视图控制器。提供可滚动的画布,用于自由绘制 PencilKit 图形和结构化标记元素。遵循 Observable 和 PKToolPickerObserver。

基本 UIKit 设置
import PaperKit
import PencilKit
import UIKit

class MarkupViewController: UIViewController, PaperMarkupViewController.Delegate {
    var paperVC: PaperMarkupViewController!
    var toolPicker: PKToolPicker!

    override func viewDidLoad() {
        super.viewDidLoad()

        let pageBounds = CGRect(origin: .zero, size: CGSize(width: 612, height: 792))
        let markup = PaperMarkup(bounds: pageBounds)
        let features = FeatureSet.latest

        paperVC = PaperMarkupViewController(
            markup: markup,
            supportedFeatureSet: features
        )
        paperVC.delegate = self

        addChild(paperVC)
        paperVC.view.frame = view.bounds
        paperVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(paperVC.view)
        paperVC.didMove(toParent: self)

        toolPicker = PKToolPicker()
        toolPicker.addObserver(paperVC)
        paperVC.pencilKitResponderState.activeToolPicker = toolPicker
        paperVC.pencilKitResponderState.toolPickerVisibility = .visible
    }

    func paperMarkupViewControllerDidChangeMarkup(
        _ controller: PaperMarkupViewController
    ) {
        guard let markup = controller.markup else { return }
        Task { try await save(markup) }
    }
}
关键属性
属性类型描述
markupPaperMarkup?当前数据模型
selectedMarkupPaperMarkup当前选中的内容
isEditableBool画布是否接受输入
isRulerActiveBool是否显示标尺叠加层
drawingToolany PKTool当前启用的 PencilKit 绘图工具
contentViewUIView? / NSView?在标记下方渲染的背景视图
zoomRangeClosedRange<CGFloat>缩放比例的最小值/最大值
supportedFeatureSetFeatureSet已启用的 PaperKit 功能
触控模式

PaperMarkupViewController.TouchMode 有两种情况:.drawing 和 .selection。

paperVC.directTouchMode = .drawing    // Finger draws
paperVC.directTouchMode = .selection  // Finger selects elements
paperVC.directTouchAutomaticallyDraws = true  // System decides based on Pencil state
内容背景

在标记图层下方设置任意视图,以显示模板、文档页面或要添加注释的图像。确保 PaperMarkup(bounds:) 的坐标空间与背景内容对齐,例如 PDF 页面或渲染图像的尺寸,以便保存的注释能够在正确位置还原:

let pageBounds = CGRect(origin: .zero, size: pageImage.size)
let imageView = UIImageView(image: pageImage)
imageView.frame = pageBounds

let markup = PaperMarkup(bounds: pageBounds)
paperVC = PaperMarkupViewController(markup: markup, supportedFeatureSet: features)
paperVC.contentView = imageView
委托回调
方法调用时机
paperMarkupViewControllerDidChangeMarkup(_:)标记内容发生变化时
paperMarkupViewControllerDidBeginDrawing(_:)用户开始绘图时
paperMarkupViewControllerDidChangeSelection(_:)选中内容发生变化时
paperMarkupViewControllerDidChangeContentVisibleFrame(_:)可见框架发生变化时

PaperMarkup 数据模型

PaperMarkup 是一个 Sendable 结构体,用于存储所有标记元素和 PencilKit 绘图数据。

创建与持久化
// New empty model. Bounds define the saved document coordinate space.
let markup = PaperMarkup(bounds: CGRect(x: 0, y: 0, width: 612, height: 792))

// Load from saved data
let markup = try PaperMarkup(dataRepresentation: savedData)

// Save — dataRepresentation() is async throws
func save(_ markup: PaperMarkup) async throws {
    let data = try await markup.dataRepresentation()
    try data.write(to: fileURL)
}
以编程方式插入内容
// Text box
markup.insertNewTextbox(
    attributedText: AttributedString("Annotation"),
    frame: CGRect(x: 50, y: 100, width: 200, height: 40),
    rotation: 0
)

// Image
markup.insertNewImage(cgImage, frame: CGRect(x: 50, y: 200, width: 300, height: 200), rotation: 0)

// Shape
let shapeConfig = ShapeConfiguration(
    type: .rectangle,
    fillColor: UIColor.systemBlue.withAlphaComponent(0.2).cgColor,
    strokeColor: UIColor.systemBlue.cgColor,
    lineWidth: 2
)
markup.insertNewShape(configuration: shapeConfig, frame: CGRect(x: 50, y: 420, width: 200, height: 100), rotation: 0)

// Line with arrow end marker
let lineConfig = ShapeConfiguration(type: .line, fillColor: nil, strokeColor: UIColor.red.cgColor, lineWidth: 3)
markup.insertNewLine(
    configuration: lineConfig,
    from: CGPoint(x: 50, y: 550), to: CGPoint(x: 250, y: 550),
    startMarker: false, endMarker: true
)

形状类型:.rectangle、.roundedRectangle、.ellipse、.line、.arrowShape、.star、.chatBubble、.regularPolygon。

其他操作
markup.append(contentsOf: otherMarkup)       // Merge another PaperMarkup
markup.append(contentsOf: pkDrawing)          // Merge a PKDrawing
markup.transformContent(CGAffineTransform(...)) // Apply affine transform
markup.removeContentUnsupported(by: featureSet) // Strip unsupported elements
属性说明
bounds标记的坐标空间
contentsRenderFrame所有内容的紧凑边界框
featureSet此数据模型的内容所用功能
indexableContent可提取并用于搜索索引的文本

在视图控制器上使用 suggestedFrameForInserting(contentInFrame:),获取一个不会与现有内容重叠的框架。

插入控制器

MarkupEditViewController(iOS、iPadOS、Mac Catalyst、visionOS)

提供弹出菜单,用于插入形状、文本框、线条和其他元素。

func showInsertionMenu(from barButtonItem: UIBarButtonItem) {
    let editVC = MarkupEditViewController(
        supportedFeatureSet: paperVC.supportedFeatureSet,
        additionalActions: []
    )
    editVC.delegate = paperVC  // PaperMarkupViewController conforms to the delegate
    editVC.modalPresentationStyle = .popover
    editVC.popoverPresentationController?.barButtonItem = barButtonItem
    present(editVC, animated: true)
}
MarkupToolbarViewController(macOS、Mac Catalyst)

提供包含绘图工具和插入按钮的工具栏。可用于原生 macOS 和采用工具栏样式 UI 的 Mac Catalyst;需要 UIKit 弹出菜单的 Catalyst 应用可以使用 MarkupEditViewController。

let toolbar = MarkupToolbarViewController(supportedFeatureSet: paperVC.supportedFeatureSet)
toolbar.delegate = paperVC
addChild(toolbar)
toolbar.view.frame = toolbarContainerView.bounds
toolbarContainerView.addSubview(toolbar.view)
toolbar.didMove(toParent: self)

这两个控制器必须使用与 PaperMarkupViewController 相同的 FeatureSet。

FeatureSet 配置

FeatureSet 控制可用的标记功能。

预设说明
.latest所有当前功能 — 建议从此预设开始
.version1版本 1 中的功能
.empty不启用任何功能
自定义
var features = FeatureSet.latest
features.remove(.stickers)
features.remove(.images)

// Or build up from empty
var features = FeatureSet.empty
features.insert(.drawing)
features.insert(.text)
features.insert(.shapeStrokes)
可用功能
功能说明
.drawing自由绘制 PencilKit 图形
.text插入文本框
.images插入图像
.stickers插入贴纸
.links链接批注
.loupes放大镜元素
.shapeStrokes形状轮廓
.shapeFills形状填充
.shapeOpacity形状不透明度控制
HDR 支持

在 FeatureSet 和 PKToolPicker 上将 colorMaximumLinearExposure 设为大于 1.0 的值:

var features = FeatureSet.latest
features.colorMaximumLinearExposure = 4.0
toolPicker.colorMaximumLinearExposure = features.colorMaximumLinearExposure

使用 view.window?.windowScene?.screen.potentialEDRHeadroom 来匹配设备屏幕的能力。仅支持 SDR 时使用 1.0。

形状、墨迹和线条标记
features.shapes = [.rectangle, .ellipse, .arrowShape, .line]
features.inks = [.pen, .pencil, .marker]
features.lineMarkerPositions = .all  // .single, .double, .plain, or .all

与 PencilKit 集成

PaperKit 接受用于绘图的 PKTool,并可追加 PKDrawing 内容。

如果应用依赖自定义笔刷行为、原始 PKDrawing / PKStroke 分析或以自定义套索为核心的编辑,PaperKit 就不能直接替代低级别的 PKCanvasView。应由 PencilKit 管理这些工作流,并在其旁边添加 PaperKit,用于结构化审阅标记,例如标注、箭头、文本框、标签、图像印章和系统标准插入 UI。只有在低级别编辑路径不再需要管理现有绘图内容时,才使用 PaperMarkup.append(contentsOf: PKDrawing) 将现有绘图迁移或复制到 PaperKit 注释层。

import PencilKit

// Set drawing tool
paperVC.drawingTool = PKInkingTool(.pen, color: .black, width: 3)

// Merge existing PKDrawing into markup
markup.append(contentsOf: existingPKDrawing)
工具选择器设置
let toolPicker = PKToolPicker()
toolPicker.addObserver(paperVC)
paperVC.pencilKitResponderState.activeToolPicker = toolPicker
paperVC.pencilKitResponderState.toolPickerVisibility = .visible

将 toolPickerVisibility 设为 .hidden,可让选择器继续工作(响应 Pencil 手势),但不显示出来,从而启用迷你工具选择器体验。

内容版本兼容性

FeatureSet.ContentVersion 映射到 PKContentVersion:

let pkVersion = features.contentVersion.pencilKitContentVersion

SwiftUI 集成

将 PaperMarkupViewController 封装在 UIViewControllerRepresentable 中:

struct MarkupView: UIViewControllerRepresentable {
    @Binding var markup: PaperMarkup
    let features: FeatureSet

    func makeUIViewController(context: Context) -> PaperMarkupViewController {
        let vc = PaperMarkupViewController(markup: markup, supportedFeatureSet: features)
        vc.delegate = context.coordinator
        let toolPicker = PKToolPicker()
        toolPicker.addObserver(vc)
        vc.pencilKitResponderState.activeToolPicker = toolPicker
        vc.pencilKitResponderState.toolPickerVisibility = .visible
        context.coordinator.toolPicker = toolPicker
        return vc
    }

    func updateUIViewController(_ vc: PaperMarkupViewController, context: Context) {
        if vc.markup != markup { vc.markup = markup }
    }

    func makeCoordinator() -> Coordinator { Coordinator(parent: self) }

    class Coordinator: NSObject, PaperMarkupViewController.Delegate {
        let parent: MarkupView
        var toolPicker: PKToolPicker?
        init(parent: MarkupView) { self.parent = parent }

        func paperMarkupViewControllerDidChangeMarkup(
            _ controller: PaperMarkupViewController
        ) {
            if let markup = controller.markup { parent.markup = markup }
        }
    }
}

在创建 SwiftUI 桥接之前,根据文档或页面尺寸初始化绑定的 PaperMarkup:

struct DocumentMarkupScreen: View {
    let pageSize: CGSize
    @State private var markup: PaperMarkup
    private let features = FeatureSet.latest

    init(pageSize: CGSize) {
        self.pageSize = pageSize
        _markup = State(
            initialValue: PaperMarkup(
                bounds: CGRect(origin: .zero, size: pageSize)
            )
        )
    }

    var body: some View {
        MarkupView(markup: $markup, features: features)
    }
}

常见错误

错误修复方法
视图、插入界面和已保存文档使用的功能集不一致选择一个受支持的 FeatureSet,并在整个编辑会话中使用它。
未经版本检查便加载内容验证 markup.featureSet.isSubset(of: supportedFeatureSet),或显示已保存的缩略图/回退内容。
序列化阻塞 UI,或发生不安全的重叠操作在交互流程之外等待 dataRepresentation(),并对自动保存进行防抖处理。
工具选取器是局部变量在控制器/视图的生命周期内保留它。
使用了不适合该平台的插入界面在 macOS 上使用 MarkupToolbarViewController;在 UIKit 上使用 MarkupEditViewController,Catalyst 支持任一呈现方式。

审核清单

  • 已添加 import PaperKit;部署目标为 iOS 26+ / macOS 26+ / visionOS 26+
  • PaperMarkup 已初始化,且其边界与内容尺寸相匹配
  • PaperMarkupViewController 和插入控制器使用相同的 FeatureSet
  • 在异步上下文中调用 dataRepresentation()
  • 将 PKToolPicker 保留为存储属性
  • 在 PaperMarkupViewController 上设置委托,以接收更改回调
  • 加载已保存的数据时检查内容版本
  • 按平台使用正确的插入控制器(macOS/Catalyst 工具栏 UI 使用 MarkupToolbarViewController;UIKit/Catalyst 弹出框使用 MarkupEditViewController)
  • 反序列化时处理 MarkupError 情况
  • HDR:在 FeatureSet 和 PKToolPicker.colorMaximumLinearExposure 上设置 colorMaximumLinearExposure

参考资料

评论

支持 Markdown