PluginBench
Skill
Pass
Audit score 90

paperkit

dpearson2699/swift-ios-skills

How to install paperkit

npx skills add https://github.com/dpearson2699/swift-ios-skills --skill paperkit
Claude Code
Cursor
Windsurf
Cline
Full instructions (SKILL.md)

Source of truth, from dpearson2699/swift-ios-skills.


name: paperkit description: "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."

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.

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

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

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.

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. 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:

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

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

Inserting Content Programmatically

// 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: .rectangle, .roundedRectangle, .ellipse, .line, .arrowShape, .star, .chatBubble, .regularPolygon.

Other Operations

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
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.

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.

let toolbar = MarkupToolbarViewController(supportedFeatureSet: paperVC.supportedFeatureSet)
toolbar.delegate = paperVC
addChild(toolbar)
toolbar.view.frame = toolbarContainerView.bounds
toolbarContainerView.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

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

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:

var features = FeatureSet.latest
features.colorMaximumLinearExposure = 4.0
toolPicker.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

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.

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

let toolPicker = PKToolPicker()
toolPicker.addObserver(paperVC)
paperVC.pencilKitResponderState.activeToolPicker = toolPicker
paperVC.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:

let pkVersion = features.contentVersion.pencilKitContentVersion

SwiftUI Integration

Wrap PaperMarkupViewController in 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 }
        }
    }
}

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

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

Mismatched FeatureSets

// 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

// 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

// 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

// 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

// DON'T — treat MarkupEditViewController as unavailable on Mac Catalyst

// DO — use the right UI for the presentation style
#if os(macOS)
let toolbar = MarkupToolbarViewController(supportedFeatureSet: features)
#elseif targetEnvironment(macCatalyst)
// Catalyst supports both: toolbar-style UI or UIKit popover insertion.
let toolbar = MarkupToolbarViewController(supportedFeatureSet: features)
let editVC = MarkupEditViewController(supportedFeatureSet: features, additionalActions: [])
#else
let editVC = MarkupEditViewController(supportedFeatureSet: features, additionalActions: [])
#endif

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

Related skills

More from dpearson2699/swift-ios-skills and the wider catalog.

SW

swiftui-animation

dpearson2699/swift-ios-skills

Implement, review, or improve SwiftUI animations and transitions. Use when adding explicit animations with withAnimation, configuring implicit animations with .animation(_:body:) or .animation(_:value:), configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (iOS 17 bounce, pulse, variableColor, scale, appear, disappear, replace; iOS 18 breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.

3.0k installsAudited
IO

ios-accessibility

dpearson2699/swift-ios-skills

Implements, reviews, or improves accessibility in iOS/macOS apps with SwiftUI, UIKit, and AppKit. Use when adding VoiceOver, Voice Control, Switch Control, or Full Keyboard Access support; when working with accessibility labels, hints, values, traits, accessibilityInputLabels, NSAccessibility, grouping, reading order, accessibility focus restoration with @AccessibilityFocusState, Dynamic Type, @ScaledMetric, custom rotors, accessibility actions, XCTest accessibility checks, App Store Accessibility Nutrition Labels, App Store Connect accessibility answers, a11y compliance audits, or system accessibility preferences.

2.7k installsAudited
SW

swiftui-patterns

dpearson2699/swift-ios-skills

Builds and reviews SwiftUI views with modern MV architecture, state management, view composition, and migration/availability guidance. Covers @Observable ownership rules, @State/@Bindable/@Environment wiring, view decomposition, custom ViewModifiers, environment values, async data loading with .task, iOS 26+ handoff reminders, Writing Tools, clipboard availability caveats, and performance guidelines. Use when structuring SwiftUI app state, managing @Observable, composing view hierarchies, or correcting SwiftUI pattern guidance.

2.6k installsAudited
SW

swiftui-performance

dpearson2699/swift-ios-skills

Audit and improve SwiftUI runtime performance. Use when diagnosing slow rendering, janky scrolling, high CPU, memory usage, excessive view updates, layout thrash, body evaluation cost, identity churn, view lifetime issues, lazy loading, Instruments profiling guidance, and performance audit requests.

2.6k installsAudited
AP

app-store-review

dpearson2699/swift-ios-skills

Prepare for App Store review and prevent rejections. Covers App Store review guidelines, app rejection reasons, PrivacyInfo.xcprivacy privacy manifest requirements, required API reason codes, in-app purchase IAP and StoreKit rules, App Store Guidelines compliance, ATT App Tracking Transparency, EU DMA Digital Markets Act, HIG compliance checklist, app submission preparation, review preparation, metadata requirements, entitlements, widgets, and Live Activities review rules. Use when preparing for App Store submission, fixing rejection reasons, auditing privacy manifests, implementing ATT consent flow, configuring StoreKit IAP, or checking HIG compliance.

2.5k installs
IO

ios-networking

dpearson2699/swift-ios-skills

Build, review, or improve networking code in iOS/macOS apps using URLSession with async/await, structured concurrency, and modern Swift patterns. Use when working with REST APIs, downloading files, uploading data, WebSocket connections, pagination, retry logic, request middleware, caching, background transfers, or network reachability monitoring. Also use when handling HTTP requests, API clients, network error handling, or data fetching in Swift apps.

2.5k installs