PaperKit
Beta-sensitive. PaperKit is new in iOS/iPadOS 26, macOS 26, and visionOS 26. API surface may change. Verify details against current Apple documentation before shipping.
PaperKit provides a unified markup experience — the same framework powering markup in Notes, Screenshots, QuickLook, and Journal. It combines PencilKit drawing with structured markup elements (shapes, text boxes, images, lines) in a single canvas managed by
PaperMarkupViewController
. Requires Swift 6.3 and the iOS 26+ SDK.
Contents
Setup
PaperKit requires no entitlements or special Info.plist entries.
Platform availability: iOS 26.0+, iPadOS 26.0+, Mac Catalyst 26.0+, macOS 26.0+, visionOS 26.0+.
Three core components:
| Component | Role |
|---|
PaperMarkupViewController
| Interactive canvas for creating and displaying markup and drawing |
| Data model for serializing all markup elements and PencilKit drawing |
/ MarkupToolbarViewController
| Insertion UI for adding markup elements |
PaperMarkupViewController
The primary view controller for interactive markup. Provides a scrollable canvas for freeform PencilKit drawing and structured markup elements. Conforms to
and
.
Basic UIKit Setup
swift
import PaperKit
import PencilKit
import UIKit
class MarkupViewController: UIViewController, PaperMarkupViewController.Delegate {
var paperVC: PaperMarkupViewController!
var toolPicker: PKToolPicker!
override func viewDidLoad() {
super.viewDidLoad()
let markup = PaperMarkup(bounds: view.bounds)
paperVC = PaperMarkupViewController(
markup: markup,
supportedFeatureSet: .latest
)
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) }
}
}
Key Properties
| Property | Type | Description |
|---|
| | The current data model |
| | Currently selected content |
| | Whether the canvas accepts input |
| | Whether the ruler overlay is shown |
| | Active PencilKit drawing tool |
| / | Background view rendered beneath markup |
| | Min/max zoom scale |
| | Enabled PaperKit features |
Touch Modes
PaperMarkupViewController.TouchMode
has two cases:
and
.
swift
paperVC.directTouchMode = .drawing // Finger draws
paperVC.directTouchMode = .selection // Finger selects elements
paperVC.directTouchAutomaticallyDraws = true // System decides based on Pencil state
Content Background
Set any view beneath the markup layer for templates, document pages, or images being annotated:
swift
paperVC.contentView = UIImageView(image: UIImage(named: "template"))
Delegate Callbacks
| Method | Called when |
|---|
paperMarkupViewControllerDidChangeMarkup(_:)
| Markup content changes |
paperMarkupViewControllerDidBeginDrawing(_:)
| User starts drawing |
paperMarkupViewControllerDidChangeSelection(_:)
| Selection changes |
paperMarkupViewControllerDidChangeContentVisibleFrame(_:)
| Visible frame changes |
PaperMarkup Data Model
is a
struct that stores all markup elements and PencilKit drawing data.
Creating and Persisting
swift
// New empty model
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)
}
Inserting Content Programmatically
swift
// 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
)
Shape types:
,
,
,
,
,
,
,
.
Other Operations
swift
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
| Property | Description |
|---|
| Coordinate space of the markup |
| Tight bounding box of all content |
| Features used by this data model's content |
| Extractable text for search indexing |
Use
suggestedFrameForInserting(contentInFrame:)
on the view controller to get a frame that avoids overlapping existing content.
Insertion Controllers
MarkupEditViewController (iOS, iPadOS, visionOS)
Presents a popover menu for inserting shapes, text boxes, lines, and other elements.
swift
func showInsertionMenu(from barButtonItem: UIBarButtonItem) {
let editVC = MarkupEditViewController(
supportedFeatureSet: .latest,
additionalActions: []
)
editVC.delegate = paperVC // PaperMarkupViewController conforms to the delegate
editVC.modalPresentationStyle = .popover
editVC.popoverPresentationController?.barButtonItem = barButtonItem
present(editVC, animated: true)
}
MarkupToolbarViewController (macOS, Mac Catalyst)
Provides a toolbar with drawing tools and insertion buttons.
swift
let toolbar = MarkupToolbarViewController(supportedFeatureSet: .latest)
toolbar.delegate = paperVC
addChild(toolbar)
toolbar.view.frame = toolbarContainerView.bounds
toolbarContainerView.addSubview(toolbar.view)
toolbar.didMove(toParent: self)
Both controllers must use the same
as the
PaperMarkupViewController
.
FeatureSet Configuration
controls which markup capabilities are available.
| Preset | Description |
|---|
| All current features — recommended starting point |
| Features from version 1 |
| No features enabled |
Customizing
swift
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)
Available Features
| Feature | Description |
|---|
| Freeform PencilKit drawing |
| Text box insertion |
| Image insertion |
| Sticker insertion |
| Link annotations |
| Loupe/magnifier elements |
| Shape outlines |
| Shape fills |
| Shape opacity control |
HDR Support
Set
colorMaximumLinearExposure
above
on both the
and
:
swift
var features = FeatureSet.latest
features.colorMaximumLinearExposure = 4.0
toolPicker.maximumLinearExposure = features.colorMaximumLinearExposure
Use
view.window?.windowScene?.screen.potentialEDRHeadroom
to match the device screen's capability. Use
for SDR-only.
Shapes, Inks, and Line Markers
swift
features.shapes = [.rectangle, .ellipse, .arrowShape, .line]
features.inks = [.pen, .pencil, .marker]
features.lineMarkerPositions = .all // .single, .double, .plain, or .all
Integration with PencilKit
PaperKit accepts
for drawing and can append
content.
swift
import PencilKit
// Set drawing tool
paperVC.drawingTool = PKInkingTool(.pen, color: .black, width: 3)
// Merge existing PKDrawing into markup
markup.append(contentsOf: existingPKDrawing)
Tool Picker Setup
swift
let toolPicker = PKToolPicker()
toolPicker.addObserver(paperVC)
paperVC.pencilKitResponderState.activeToolPicker = toolPicker
paperVC.pencilKitResponderState.toolPickerVisibility = .visible
Setting
to
keeps the picker functional (responds to Pencil gestures) but not visible, enabling the mini tool picker experience.
Content Version Compatibility
FeatureSet.ContentVersion
maps to
:
swift
let pkVersion = features.contentVersion.pencilKitContentVersion
SwiftUI Integration
Wrap
PaperMarkupViewController
in
UIViewControllerRepresentable
:
swift
struct MarkupView: UIViewControllerRepresentable {
@Binding var markup: PaperMarkup
func makeUIViewController(context: Context) -> PaperMarkupViewController {
let vc = PaperMarkupViewController(markup: markup, supportedFeatureSet: .latest)
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 }
}
}
}
Common Mistakes
Mismatched FeatureSets
swift
// DON'T
let paperVC = PaperMarkupViewController(markup: m, supportedFeatureSet: .latest)
let editVC = MarkupEditViewController(supportedFeatureSet: .version1, additionalActions: [])
// DO — use the same FeatureSet for both
let features = FeatureSet.latest
let paperVC = PaperMarkupViewController(markup: m, supportedFeatureSet: features)
let editVC = MarkupEditViewController(supportedFeatureSet: features, additionalActions: [])
Ignoring Content Version on Load
swift
// DON'T
let markup = try PaperMarkup(dataRepresentation: data)
paperVC.markup = markup
// DO — check version compatibility
let markup = try PaperMarkup(dataRepresentation: data)
if markup.featureSet.isSubset(of: paperVC.supportedFeatureSet) {
paperVC.markup = markup
} else {
showVersionMismatchAlert()
}
Blocking Main Thread with Serialization
swift
// DON'T — dataRepresentation() is async, don't try to work around it
// DO — save from an async context
func paperMarkupViewControllerDidChangeMarkup(_ controller: PaperMarkupViewController) {
guard let markup = controller.markup else { return }
Task {
let data = try await markup.dataRepresentation()
try data.write(to: fileURL)
}
}
Forgetting to Retain the Tool Picker
swift
// DON'T — local variable gets deallocated
func viewDidLoad() {
let toolPicker = PKToolPicker()
toolPicker.addObserver(paperVC)
}
// DO — store as instance property
var toolPicker: PKToolPicker!
Wrong Insertion Controller for Platform
swift
// DON'T — MarkupEditViewController is iOS/iPadOS/visionOS only
// DO
#if os(macOS)
let toolbar = MarkupToolbarViewController(supportedFeatureSet: features)
#else
let editVC = MarkupEditViewController(supportedFeatureSet: features, additionalActions: [])
#endif
Review Checklist
References
- PaperKit documentation
- Integrating PaperKit into your app
- Meet PaperKit — WWDC25
- The skill covers PencilKit drawing, tool pickers, and PKDrawing serialization
- references/paperkit-patterns.md — data persistence, rendering, multi-platform setup, custom feature sets