WWDC Quick Look đź’“ By SwiftGGTeam
Meet PaperKit

Meet PaperKit

Watch original video

Highlight

PaperKit is Apple’s own markup framework. Until now it shipped only inside system apps (Notes, Screenshots, QuickLook, Journal). Third-party apps can now integrate it directly.


Core Content

To build a “draw a stroke + add a shape + drop in an image + insert a text box” experience like Notes or Screenshot Markup, you used to start from scratch: manage layers, mix PencilKit drawings with markup elements, design your own insert menu, port everything to macOS. A non-core feature easily ran to several thousand lines of code.

At WWDC25, Apple opened up PaperKit, the framework it has used for years (00:07). It is the foundation of Apple’s system-level markup experience. Notes, Screenshots, QuickLook, and Journal all run on it. macOS Tahoe now supports it natively as well (00:57).

The framework has three core components. PaperMarkupViewController is the interactive canvas; it shows and edits markup and PencilKit drawings. PaperMarkup is the data-model container; it handles save, load, and rendering. MarkupEditViewController (iOS / iPadOS / visionOS) and MarkupToolbarViewController (macOS) handle inserting shapes, images, text boxes, and other elements (02:31). Wire these three together and you get the same markup capabilities as the system apps.


Detailed Content

The minimal iOS integration fits in one viewDidLoad (03:47).

override func viewDidLoad() {
    super.viewDidLoad()

    let markupModel = PaperMarkup(bounds: view.bounds)
    let paperViewController = PaperMarkupViewController(markup: markupModel, supportedFeatureSet: .latest)
    view.addSubview(paperViewController.view)
    addChild(paperViewController)
    paperViewController.didMove(toParent: self)
    becomeFirstResponder()

    let toolPicker = PKToolPicker()
    toolPicker.addObserver(paperViewController)

    pencilKitResponderState.activeToolPicker = toolPicker
    pencilKitResponderState.toolPickerVisibility = .visible

    toolPicker.accessoryItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(plusButtonPressed(_:)))
}

Key points:

  • PaperMarkup(bounds:) creates the data model. Pass view.bounds so the rendering context is correct.
  • PaperMarkupViewController(markup:supportedFeatureSet:) creates the canvas. Pass .latest to get every current markup capability.
  • addChild + didMove(toParent:) is the standard way to attach a UIKit container view controller.
  • PKToolPicker registers the canvas as an observer through addObserver, so the canvas reacts when the tool changes.
  • pencilKitResponderState.activeToolPicker is a new API on UIResponder for setting the current responder’s tool picker (04:32).
  • toolPickerVisibility controls visibility. When hidden, double-tap and squeeze gestures still work, which makes a mini tool picker easy to build (05:03).
  • toolPicker.accessoryItem puts a + button on the tool picker as the entry point for the insert menu.

The macOS structure is almost the same. The only difference is that the insert UI uses a toolbar instead of a popover menu (06:11).

override func viewDidLoad() {
    super.viewDidLoad()

    let markupModel = PaperMarkup(bounds: view.bounds)
    let paperViewController = PaperMarkupViewController(markup: markupModel, supportedFeatureSet: .latest)
    view.addSubview(paperViewController.view)
    addChild(paperViewController)

    let toolbarViewController = MarkupToolbarViewController(supportedFeatureSet: .latest)
    toolbarViewController.delegate = paperViewController
    view.addSubview(toolbarViewController.view)

    setupLayoutConstraints()
}

Key points:

  • macOS uses MarkupToolbarViewController in place of the iOS popover MarkupEditViewController.
  • toolbarViewController.delegate = paperViewController wires the toolbar’s insert callbacks to the canvas.
  • The canvas and the toolbar share the same FeatureSet, so the available tools stay consistent.

Save data with a delegate callback (07:18).

func paperMarkupViewControllerDidChangeMarkup(_ paperMarkupViewController: PaperMarkupViewController) {
    let markupModel = paperMarkupViewController.markup
    Task {
        let data = try await markupModel.dataRepresentation()
        try data.write(toFile: paperKitDataURL)
    }
}

Key points:

  • paperMarkupViewControllerDidChangeMarkup fires every time the canvas changes.
  • markupModel.dataRepresentation() is async and returns Data, which you can write straight to disk.
  • PaperMarkupViewController also conforms to Observable, so you can swap the delegate for a state-driven approach if you prefer (07:33).

For forward compatibility, save a thumbnail and show it when the version does not match (08:02).

func updateThumbnail(_ markupModel: PaperMarkup) async throws {
    let thumbnailSize = CGSize(width: 200, height: 200)
    let context = makeCGContext(size: thumbnailSize)
    context.setFillColor(gray: 1, alpha: 1)
    context.fill(renderer.format.bounds)

    await markupModel.draw(in: context, frame: CGRect(origin: .zero, size: thumbnailSize))

    thumbnail = context.makeImage()
}

Key points:

  • Create your own CGContext as the render target.
  • markupModel.draw(in:frame:) is async and draws the current model into the context.
  • Save the resulting CGImage alongside the markup data. This is what Notes and other system apps do.

To customize the tool set, subtract from .latest (09:02).

var featureSet: FeatureSet = .latest

featureSet.remove(.text)
featureSet.insert(.stickers)

featureSet.colorMaximumLinearExposure = 4
toolPicker.colorMaximumLinearExposure = 4

let paperViewController = PaperMarkupViewController(supportedFeatureSet: featureSet)
let markupEditViewController = MarkupEditViewController(supportedFeatureSet: featureSet)

Key points:

  • FeatureSet.latest includes every current capability, and your app picks up future additions automatically.
  • remove and insert add or drop specific capabilities, such as removing the text box or adding stickers.
  • Setting colorMaximumLinearExposure greater than 1 turns on HDR markup. Set it on both the canvas and the tool picker (09:30).
  • The recommended value of 4 is what the speaker uses in the sample app. You can also read the actual headroom from UIScreen or NSScreen.
  • Turning on HDR also unlocks the new PencilKit calligraphy pen, the Reed tool (10:22).

The last common extension point is a custom background (10:50).

let template = UIImage(named: "MyTemplate.jpg")
let templateView = UIImageView(image: template)
paperViewController.contentView = templateView

Key points:

  • contentView accepts any UIView. All markup and drawings render on top of it.
  • It fits “fixed background + drawing on top” cases such as template paper (ruled lines, dot grids, recipe cards).

In SwiftUI, wrap it with UIViewControllerRepresentable to plug it in (06:46).


Core Takeaways

1. Use PaperKit to add Apple-style markup to notes, reading, and education apps in one shot

Why it matters: building your own mix of handwriting, shapes, and images used to be a huge job. With PaperKit, a few dozen lines of code reuse the Notes / Journal experience.

How to start: get the minimal iOS integration working (PaperMarkup + PaperMarkupViewController + PKToolPicker), then use the delegate callback to persist data to your storage layer.

2. Start from FeatureSet.latest and subtract, instead of adding to an empty set

Why it matters: .latest follows the framework as it evolves, so future tools arrive without code changes. Building up from an empty set with insert makes it easy to miss new features and costs more to maintain.

How to start: declare var featureSet: FeatureSet = .latest and only remove the tools your app does not need. A pure reading-annotation app can remove(.stickers).

3. Save a thumbnail for forward compatibility, so newer data does not crash or show blank in older versions

Why it matters: a user might edit on iOS 27 and open the same file on iOS 26, where new elements cannot be parsed. A thumbnail on disk guarantees that older versions can at least show the content.

How to start: in the delegate callback, call both markupModel.dataRepresentation() and the thumbnail render, and save the two together. On load, check the version and fall back to the thumbnail if it does not match.

4. Use pencilKitResponderState.toolPickerVisibility to build a mini tool picker

Why it matters: hiding the tool picker keeps double-tap and squeeze gestures working, so you can ship a tighter custom toolbar UI without losing interactions.

How to start: hide the tool picker, draw your own simplified tool buttons, and have those buttons call the PKToolPicker state API to switch tools.


Comments

GitHub Issues · utterances