Paperkit

by dpearson26998d90fd121a26No license1.1K starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 2 months ago

Add drawings, shapes, and a consistent markup experience using PaperKit. Use when integrating PaperMarkupViewController for markup editing, adding shape recognition, working with PaperMarkup data models, embedding markup tools in document editors, or building annotation features that need the system-standard markup toolbar. New in iOS 26.

Instructions onlySoftware Development
AI-generated overview

Guides developers integrating Apple's PaperKit framework for markup, shapes, and drawing in iOS 26 apps.

What it does
This skill provides reference guidance for using Apple's PaperKit framework, covering PaperMarkupViewController, the PaperMarkup data model, insertion controllers, FeatureSet configuration, PencilKit integration, and SwiftUI bridging. It includes code samples, common mistakes, and a review checklist for building markup and annotation features. It produces developer-facing instructions rather than files or artifacts.
When to use it
Use it when integrating PaperMarkupViewController for markup editing, adding shape recognition, working with PaperMarkup data models, or embedding markup tools in document editors. It is aimed at apps targeting iOS, iPadOS, macOS, or visionOS 26 that need the system-standard markup toolbar.
Requirements
Requires an Apple platform development environment with iOS 26+, iPadOS 26+, Mac Catalyst 26+, macOS 26+, or visionOS 26+ and the PaperKit and PencilKit frameworks. It ships no scripts; it is instructions only, with an optional reference file.

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 combines PencilKit drawing with structured markup elements such as shapes, text, images, and lines in a canvas managed by PaperMarkupViewController.

Contents

Workflow

  1. Choose the document bounds, supported FeatureSet, and persistence version before constructing UI.
  2. Create PaperMarkup, embed PaperMarkupViewController, and keep the controller, tool picker, and insertion controller alive for the view lifetime.
  3. Use the platform-appropriate insertion surface and keep PencilKit drawing inside the PaperKit document boundary.
  4. Save off the main thread, retain a thumbnail for forward-incompatible content, and test round-trip loading with the same feature set.
  5. On failure, restore the original document bytes, fix the feature-set/version/controller mismatch, and rerun edit, save, relaunch, load, thumbnail fallback, and undo checks.

Load references/paperkit-patterns.md [blocked] for full platform setup, tool picker wiring, persistence, thumbnails, custom feature sets, programmatic construction, and migration.

Setup

PaperKit requires no entitlements or special Info.plist entries.

swift
import PaperKit

Platform availability: iOS 26.0+, iPadOS 26.0+, Mac Catalyst 26.0+, macOS 26.0+, visionOS 26.0+.

Three core components:

ComponentRole
PaperMarkupViewControllerInteractive canvas for creating and displaying markup and drawing
PaperMarkupData model for serializing all markup elements and PencilKit drawing
MarkupEditViewController / MarkupToolbarViewControllerInsertion 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 Observable and PKToolPickerObserver.

Basic UIKit Setup

swift
import PaperKitimport PencilKitimport 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) }    }}

Key Properties

PropertyTypeDescription
markupPaperMarkup?The current data model
selectedMarkupPaperMarkupCurrently selected content
isEditableBoolWhether the canvas accepts input
isRulerActiveBoolWhether the ruler overlay is shown
drawingToolany PKToolActive PencilKit drawing tool
contentViewUIView? / NSView?Background view rendered beneath markup
zoomRangeClosedRange<CGFloat>Min/max zoom scale
supportedFeatureSetFeatureSetEnabled PaperKit features

Touch Modes

PaperMarkupViewController.TouchMode has two cases: .drawing and .selection.

swift
paperVC.directTouchMode = .drawing    // Finger drawspaperVC.directTouchMode = .selection  // Finger selects elementspaperVC.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. Keep the PaperMarkup(bounds:) coordinate space aligned to the background content, such as a PDF page or rendered image size, so saved annotations restore in the right place:

swift
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

MethodCalled when
paperMarkupViewControllerDidChangeMarkup(_:)Markup content changes
paperMarkupViewControllerDidBeginDrawing(_:)User starts drawing
paperMarkupViewControllerDidChangeSelection(_:)Selection changes
paperMarkupViewControllerDidChangeContentVisibleFrame(_:)Visible frame changes

PaperMarkup Data Model

PaperMarkup is a Sendable struct that stores all markup elements and PencilKit drawing data.

Creating and Persisting

swift
// 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 datalet markup = try PaperMarkup(dataRepresentation: savedData)
// Save — dataRepresentation() is async throwsfunc save(_ markup: PaperMarkup) async throws {    let data = try await markup.dataRepresentation()    try data.write(to: fileURL)}

Inserting Content Programmatically

swift
// Text boxmarkup.insertNewTextbox(    attributedText: AttributedString("Annotation"),    frame: CGRect(x: 50, y: 100, width: 200, height: 40),    rotation: 0)
// Imagemarkup.insertNewImage(cgImage, frame: CGRect(x: 50, y: 200, width: 300, height: 200), rotation: 0)
// Shapelet 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 markerlet 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: .rectangle, .roundedRectangle, .ellipse, .line, .arrowShape, .star, .chatBubble, .regularPolygon.

Other Operations

swift
markup.append(contentsOf: otherMarkup)       // Merge another PaperMarkupmarkup.append(contentsOf: pkDrawing)          // Merge a PKDrawingmarkup.transformContent(CGAffineTransform(...)) // Apply affine transformmarkup.removeContentUnsupported(by: featureSet) // Strip unsupported elements
PropertyDescription
boundsCoordinate space of the markup
contentsRenderFrameTight bounding box of all content
featureSetFeatures used by this data model's content
indexableContentExtractable 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, Mac Catalyst, visionOS)

Presents a popover menu for inserting shapes, text boxes, lines, and other elements.

swift
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)

Provides a toolbar with drawing tools and insertion buttons. Use it for native macOS and for Mac Catalyst toolbar-style UI; Catalyst apps that want a UIKit popover can use MarkupEditViewController.

swift
let toolbar = MarkupToolbarViewController(supportedFeatureSet: paperVC.supportedFeatureSet)toolbar.delegate = paperVCaddChild(toolbar)toolbar.view.frame = toolbarContainerView.boundstoolbarContainerView.addSubview(toolbar.view)toolbar.didMove(toParent: self)

Both controllers must use the same FeatureSet as the PaperMarkupViewController.

FeatureSet Configuration

FeatureSet controls which markup capabilities are available.

PresetDescription
.latestAll current features — recommended starting point
.version1Features from version 1
.emptyNo features enabled

Customizing

swift
var features = FeatureSet.latestfeatures.remove(.stickers)features.remove(.images)
// Or build up from emptyvar features = FeatureSet.emptyfeatures.insert(.drawing)features.insert(.text)features.insert(.shapeStrokes)

Available Features

FeatureDescription
.drawingFreeform PencilKit drawing
.textText box insertion
.imagesImage insertion
.stickersSticker insertion
.linksLink annotations
.loupesLoupe/magnifier elements
.shapeStrokesShape outlines
.shapeFillsShape fills
.shapeOpacityShape opacity control

HDR Support

Set colorMaximumLinearExposure above 1.0 on both the FeatureSet and PKToolPicker:

swift
var features = FeatureSet.latestfeatures.colorMaximumLinearExposure = 4.0toolPicker.colorMaximumLinearExposure = features.colorMaximumLinearExposure

Use view.window?.windowScene?.screen.potentialEDRHeadroom to match the device screen's capability. Use 1.0 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 PKTool for drawing and can append PKDrawing content.

PaperKit is not a drop-in replacement for a low-level PKCanvasView when the app depends on custom brush behavior, raw PKDrawing / PKStroke analytics, or custom lasso-centric editing. Keep those workflows owned by PencilKit, and add PaperKit beside them for structured review markup such as callouts, arrows, text boxes, labels, image stamps, and system-standard insertion UI. Migrate or duplicate existing drawings into a PaperKit annotation layer with PaperMarkup.append(contentsOf: PKDrawing) only when the low-level editing path no longer needs to own that content.

swift
import PencilKit
// Set drawing toolpaperVC.drawingTool = PKInkingTool(.pen, color: .black, width: 3)
// Merge existing PKDrawing into markupmarkup.append(contentsOf: existingPKDrawing)

Tool Picker Setup

swift
let toolPicker = PKToolPicker()toolPicker.addObserver(paperVC)paperVC.pencilKitResponderState.activeToolPicker = toolPickerpaperVC.pencilKitResponderState.toolPickerVisibility = .visible

Setting toolPickerVisibility to .hidden keeps the picker functional (responds to Pencil gestures) but not visible, enabling the mini tool picker experience.

Content Version Compatibility

FeatureSet.ContentVersion maps to PKContentVersion:

swift
let pkVersion = features.contentVersion.pencilKitContentVersion

SwiftUI Integration

Wrap PaperMarkupViewController in UIViewControllerRepresentable:

swift
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 }        }    }}

Initialize the bound PaperMarkup from the document or page size before creating the SwiftUI bridge:

swift
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)    }}

Common Mistakes

MistakeFix
View, insertion UI, and saved document use mismatched feature setsChoose one supported FeatureSet and use it across the editing session.
Loaded content is assigned without a version checkVerify markup.featureSet.isSubset(of: supportedFeatureSet) or show the saved thumbnail/fallback.
Serialization blocks UI or overlaps unsafelyAwait dataRepresentation() off the interaction path and debounce autosaves.
Tool picker is a local variableRetain it for the controller/view lifetime.
Wrong insertion surface for the platformUse MarkupToolbarViewController on macOS; use MarkupEditViewController on UIKit, with Catalyst supporting either presentation.

Review Checklist

  • import PaperKit present; deployment target is iOS 26+ / macOS 26+ / visionOS 26+
  • PaperMarkup initialized with bounds matching content size
  • Same FeatureSet used for PaperMarkupViewController and insertion controller
  • dataRepresentation() called in async context
  • PKToolPicker retained as a stored property
  • Delegate set on PaperMarkupViewController for change callbacks
  • Content version checked when loading saved data
  • Correct insertion controller per platform (MarkupToolbarViewController for macOS/Catalyst toolbar UI; MarkupEditViewController for UIKit/Catalyst popovers)
  • MarkupError cases handled on deserialization
  • HDR: colorMaximumLinearExposure set on FeatureSet and PKToolPicker.colorMaximumLinearExposure

References

Source and attribution

Source:dpearson2699/swift-ios-skillsinskills/paperkitat commit8d90fd1

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal

More from dpearson2699/swift-ios-skills

Widgetkit

dpearson2699

Guides implementing, reviewing, and improving WidgetKit widgets and controls for iOS, iPadOS, watchOS, and CarPlay.

Software Development1.1Kupdated 2 months ago

Weatherkit

dpearson2699

Guides iOS developers in fetching WeatherKit forecasts, alerts, and attribution using WeatherService.

Software Development1.1Kupdated 2 months ago

Vision Framework

dpearson2699

Implement computer vision features including text recognition (OCR), face detection, barcode scanning, image segmentation, object tracking, and document scanning in iOS apps. Covers both the modern Swift-native Vision API (iOS 18+) and legacy VNRequest patterns, VisionKit DataScannerViewController for live camera scanning, and CoreMLRequest/VNCoreMLRequest for custom model inference. Use when adding OCR, barcode scanning, face detection, or custom Core ML model inference with Vision.

Awaiting classification1.1Kupdated 2 months ago

Tipkit

dpearson2699

Implement and review Apple TipKit feature-discovery UI for iOS 17+ apps. Use when adding or auditing in-app tips, contextual help, coach marks, Tip, TipView, popoverTip, rules, events, actions, display frequency, testing overrides, reusable tip identifiers, or iOS 18+ TipGroup and CloudKit tip sync; avoid for generic SwiftUI navigation or layout outside tip presentation.

Awaiting classification1.1Kupdated 2 months ago

Tabletopkit

dpearson2699

Guides building multiplayer spatial board games on visionOS with Apple's TabletopKit and RealityKit.

Software Development1.1Kupdated 2 months ago

Swiftui Webkit

dpearson2699

Guides embedding and controlling web content in SwiftUI apps with WebKit for SwiftUI on iOS 26 and later.

Software Development1.1Kupdated 2 months ago