使用 PaperKit 新增手繪、形狀,並提供一致的標記體驗。適用於整合 PaperMarkupViewController 進行標記編輯、加入形狀辨識、處理 PaperMarkup 資料模型、在文件編輯器中嵌入標記工具,或建置需要系統標準標記工具列的標註功能。iOS 26 全新推出。
PaperKit
Beta 敏感提醒。 PaperKit 為 iOS/iPadOS 26、macOS 26 及 visionOS 26 的全新功能。API 介面可能會有所變更。在正式發布前,請務必參閱最新的 Apple 官方文件確認細節。
PaperKit 將 PencilKit 的繪圖功能與結構化標記元素(如形狀、文字、影像及線條)整合於由 PaperMarkupViewController 管理的畫布中。
目錄
- 設定
- 工作流程
- PaperMarkupViewController
- PaperMarkup 資料模型
- 插入控制器
- FeatureSet 設定
- 與 PencilKit 整合
- SwiftUI 整合
- 常見錯誤
- 檢查清單
- 參考資料
工作流程
- 在建構 UI 之前,請先確定文件邊界(bounds)、支援的
FeatureSet以及持久化版本。 - 建立
PaperMarkup並嵌入PaperMarkupViewController,在 View 的生命週期內保持 Controller、工具選擇器(Tool Picker)與插入控制器的存活狀態。 - 使用適合該平台的插入介面,並將 PencilKit 繪圖限制在 PaperKit 文件邊界內。
- 於主執行緒之外進行儲存,為不向前相容的內容保留縮圖,並使用相同的功能集測試來回(Round-trip)載入。
- 若發生失敗,請還原原始文件位元組,修復功能集/版本/控制器不符合的問題,並重新執行編輯、儲存、重新啟動、載入、縮圖備援及復原(Undo)檢查。
載入 references/paperkit-patterns.md 以取得完整的平台設定、工具選擇器連接、持久化、縮圖、自訂功能集、程式化建構及轉移指南。
設定
PaperKit 不需要任何特殊權限(Entitlements)或 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) }
}
}
重要屬性
| 屬性 | 型別 | 說明 |
|---|---|---|
markup |
PaperMarkup? |
目前的資料模型 |
selectedMarkup |
PaperMarkup |
目前選取的內容 |
isEditable |
Bool |
畫布是否接受輸入 |
isRulerActive |
Bool |
是否顯示尺規覆蓋層 |
drawingTool |
any PKTool |
目前啟用的 PencilKit 繪圖工具 |
contentView |
UIView? / NSView? |
渲染於標記下方的背景視圖 |
zoomRange |
ClosedRange<CGFloat> |
最小/最大縮放比例 |
supportedFeatureSet |
FeatureSet |
已啟用的 PaperKit 功能集 |
觸控模式
PaperMarkupViewController.TouchMode 包含兩種情況:.drawing 與 .selection。
paperVC.directTouchMode = .drawing // 手指繪圖
paperVC.directTouchMode = .selection // 手指選取元素
paperVC.directTouchAutomaticallyDraws = true // 系統根據 Pencil 狀態自動決定
內容背景
可在標記層下方設定任何視圖,作為範本、文件頁面或欲進行標註的影像。請確保 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
Delegate 回呼(Callbacks)
| 方法 | 呼叫時機 |
|---|---|
paperMarkupViewControllerDidChangeMarkup(_:) |
標記內容發生變更時 |
paperMarkupViewControllerDidBeginDrawing(_:) |
使用者開始繪圖時 |
paperMarkupViewControllerDidChangeSelection(_:) |
選取內容發生變更時 |
paperMarkupViewControllerDidChangeContentVisibleFrame(_:) |
可見框線範圍發生變更時 |
PaperMarkup 資料模型
PaperMarkup 是一個 Sendable 結構,用於儲存所有標記元素與 PencilKit 繪圖資料。
建立與持久化
// 建立新的空白模型。Bounds 用於定義儲存文件的座標空間。
let markup = PaperMarkup(bounds: CGRect(x: 0, y: 0, width: 612, height: 792))
// 從已儲存的資料載入
let markup = try PaperMarkup(dataRepresentation: savedData)
// 儲存 — dataRepresentation() 為 async throws
func save(_ markup: PaperMarkup) async throws {
let data = try await markup.dataRepresentation()
try data.write(to: fileURL)
}
以程式化方式插入內容
// 文字方塊
markup.insertNewTextbox(
attributedText: AttributedString("Annotation"),
frame: CGRect(x: 50, y: 100, width: 200, height: 40),
rotation: 0
)
// 影像
markup.insertNewImage(cgImage, frame: CGRect(x: 50, y: 200, width: 300, height: 200), rotation: 0)
// 形狀
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)
// 附帶箭頭端點標記的線條
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) // 合併另一個 PaperMarkup
markup.append(contentsOf: pkDrawing) // 合併 PKDrawing
markup.transformContent(CGAffineTransform(...)) // 套用仿射轉換 (Affine transform)
markup.removeContentUnsupported(by: featureSet) // 移除不被 featureSet 支援的元素
| 屬性 | 說明 |
|---|---|
bounds |
標記的座標空間 |
contentsRenderFrame |
包覆所有內容的緊密邊界框(Bounding box) |
featureSet |
此資料模型內容所使用的功能集 |
indexableContent |
可擷取用於搜尋索引的文字 |
可在視圖控制器上使用 suggestedFrameForInserting(contentInFrame:) 來取得能避免與既有內容重疊的建議 Frame。
插入控制器
MarkupEditViewController (iOS, iPadOS, Mac Catalyst, visionOS)
顯示彈出式功能表(Popover menu),用於插入形狀、文字方塊、線條及其他元素。
func showInsertionMenu(from barButtonItem: UIBarButtonItem) {
let editVC = MarkupEditViewController(
supportedFeatureSet: paperVC.supportedFeatureSet,
additionalActions: []
)
editVC.delegate = paperVC // PaperMarkupViewController 符合此 delegate 協定
editVC.modalPresentationStyle = .popover
editVC.popoverPresentationController?.barButtonItem = barButtonItem
present(editVC, animated: true)
}
MarkupToolbarViewController (macOS, Mac Catalyst)
提供包含繪圖工具與插入按鈕的工具列。適用於原生 macOS 以及 Mac Catalyst 工具列風格的 UI;若 Mac Catalyst 應用程式想使用 UIKit Popover,可以使用 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)
// 或從空集合開始組建
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, 或 .all
與 PencilKit 整合
PaperKit 接受使用 PKTool 進行繪圖,且可以附加 PKDrawing 內容。
當應用程式依賴自訂筆刷行為、原始 PKDrawing / PKStroke 分析,或自訂以套索為中心(Lasso-centric)的編輯時,PaperKit 並不能直接作為低階 PKCanvasView 的無縫替代方案。請讓 PencilKit 繼續維護這些工作流程,並在其旁加入 PaperKit,以實現結構化的審閱標記,例如圖解說明(Callouts)、箭頭、文字方塊、標籤、影像印章及系統標準的插入 UI。只有當低階編輯路徑不再需要擁有該內容時,才將既有繪圖轉移或複製到 PaperKit 標註層(使用 PaperMarkup.append(contentsOf: PKDrawing))。
import Penci
<!-- truncated for translation batch; full body continues in source -->






