WWDC.ai

Unwrap PaperKit

Use PaperKit's new data model, markup elements, and adornments to build editable canvas apps with locked templates and custom overlays.

Watch on Apple Developer

TL;DR

  • PaperMarkup now exposes subelements as a readable and writable MarkupOrderedSet, letting apps create, inspect, reorder, and update canvas contents programmatically.
  • Every canvas item conforms to Markup; allowedInteractions lets apps selectively permit moving, resizing, rotating, deleting, styling, or selecting, with .readOnly for locked template content.
  • Concrete markup element types include shapes, images, links, loupes, and PencilKit strokes, each with type-specific properties such as shape stroke and fill colors.
  • MarkupAdornment adds non-persisted interactive overlays anchored to canvas coordinates, useful for buttons, annotations, collaboration UI, and integrations such as Image Playground.

PaperKit as an app canvas

PaperKit is the canvas technology used across Apple apps such as Notes, Preview, and Freeform, and is available to apps on iOS, macOS, and visionOS 27. The session demonstrates a comic book editor that turns page templates into editable PaperKit canvases.

The main workflow is to create PaperMarkup, populate it with markup elements, configure how those elements can be interacted with, then layer non-document controls on top using adornments.

  • Use PaperMarkup as the document-level canvas model.
  • Use PaperMarkupViewController.markup to display and update canvas contents.
  • Use PaperKit when the app needs a mixed canvas of Pencil input, shapes, text, images, and other markup elements.

Build markup with the data model

PaperMarkup has a subelements property that exposes every canvas element as a MarkupOrderedSet. The ordered set can be read and written, so apps can generate markup from their own models and assign the result back to the controller.

In the comic editor example, each panel rectangle is converted into a ShapeMarkup and appended to the markup's subelements.

  • Create PaperMarkup with bounds matching the page size.
  • Read markup.subelements into a mutable MarkupOrderedSet.
  • Append generated elements, then assign the ordered set back to markup.subelements.
  • Assign the completed markup to the PaperKit view controller.

Create shape subelements for a template

Generates a PaperMarkup page by appending one ShapeMarkup per comic panel.

import PaperKit

func generateMarkup(pageSize: CGSize,
                    panelFrames: [CGRect],
                    configuration: ShapeConfiguration) -> PaperMarkup {
    var markup = PaperMarkup(bounds: CGRect(origin: .zero, size: pageSize))
    var subelements: MarkupOrderedSet = markup.subelements

    for panelFrame in panelFrames {
        let shape = ShapeMarkup(frame: panelFrame, configuration: configuration)
        subelements.append(shape)
    }

    markup.subelements = subelements
    return markup
}

Lock template elements with allowed interactions

Every element on the canvas conforms to the Markup protocol, which provides common properties such as frame and rotation. The new allowedInteractions property uses the MarkupInteractions option set to control what users can modify per element.

Interactions can be controlled independently for moving, resizing and rotating, deleting, styling, and selecting. The .readOnly convenience value locks down all of them, which is useful for template content such as comic panel borders while leaving other user-created elements editable.

  • Use allowedInteractions on individual elements, not just the whole canvas.
  • Set template elements to .readOnly when they should remain visible but not selectable, movable, stylable, or deletable.
  • Keep user-added elements, such as speech bubbles or drawings, interactive if the app's editing model requires it.

Make generated template shapes read-only

Locks the generated panel shapes so the user cannot select, move, style, or delete the template.

import PaperKit

func generateMarkup(pageSize: CGSize,
                    panelFrames: [CGRect],
                    configuration: ShapeConfiguration) -> PaperMarkup {
    var markup = PaperMarkup(bounds: CGRect(origin: .zero, size: pageSize))
    var subelements: MarkupOrderedSet = markup.subelements

    for panelFrame in panelFrames {
        var shape = ShapeMarkup(frame: panelFrame, configuration: configuration)
        shape.allowedInteractions = .readOnly
        subelements.append(shape)
    }

    markup.subelements = subelements
    return markup
}

Work with concrete element types

Although all elements live in the same MarkupOrderedSet and conform to Markup, each element has a concrete type with its own properties. The session calls out shapes, images, links, loupes, and pencil strokes.

For shape-based templates, apps can cast elements to ShapeMarkup and update properties such as stroke and fill colors. The example also updates the markup background color to match the selected style.

  • Iterate through markup.subelements to inspect and modify existing canvas elements.
  • Cast to concrete types such as ShapeMarkup or ImageMarkup when type-specific properties are needed.
  • Use updateOrAppend(_:) after changing an element in a MarkupOrderedSet.
  • PaperKit builds on PencilKit; Apple Pencil strokes become markup elements and can use PencilKit model APIs.

Apply a selected color to panel shapes

Finds shape elements, updates their style, writes the ordered set back to the markup, and refreshes the controller.

import PaperKit

func updatePanelColor(_ selectedColor: CGColor) {
    guard var markup: PaperMarkup = paperMarkupViewController.markup else { return }
    var subelements: MarkupOrderedSet = markup.subelements

    for element in subelements {
        guard var shape = element as? ShapeMarkup else { continue }
        shape.strokeColor = selectedColor
        shape.fillColor = selectedColor.copy(alpha: 0.15)
        subelements.updateOrAppend(shape)
    }

    markup.subelements = subelements
    markup.backgroundColor = selectedColor.copy(alpha: 0.15)
    paperMarkupViewController.markup = markup
}

Add custom controls with adornments

MarkupAdornment provides a visual overlay anchored to canvas coordinates. Adornments track zoom and scroll, but remain separate from persisted markup, so they are not saved, printed, or exported as document content.

The comic editor adds an adornment button to each panel using an SF Symbol image configuration. Taps are handled through the PaperKit view controller delegate; the example launches Image Playground, then inserts the generated image as an ImageMarkup into the canvas.

  • Use adornments for editing-only UI such as buttons, annotations, and collaboration controls.
  • Anchor adornments to canvas positions, such as the center of each panel.
  • Assign adornments through paperMarkupViewController.adornments.
  • Maintain your own mapping from adornment IDs to app model objects, such as panel indexes.

Create one adornment per panel

Adds non-persisted overlay controls anchored to panel centers.

import PaperKit

func addPanelAdornments(for page: Page) {
    var adornments: [MarkupAdornment] = []

    for (panelIndex, panel) in page.panels.enumerated() {
        let adornmentID = UUID()
        adornmentPanelMapping[adornmentID] = panelIndex

        let center = CGPoint(x: panel.midX, y: panel.midY)
        let adornment = MarkupAdornment(
            id: adornmentID,
            anchor: .canvas(location: center),
            imageConfiguration: .systemImage("photo.badge.plus"),
            dragRegion: .fixed,
            scalesWithZoom: false
        )
        adornments.append(adornment)
    }

    paperMarkupViewController.adornments = adornments
}

Respond to an adornment and insert generated artwork

Uses an adornment tap to present Image Playground, then stores the generated image as persisted ImageMarkup content.

import ImagePlayground
import PaperKit

func paperMarkupViewController(_ paperMarkupViewController: PaperMarkupViewController,
                               didTapAdornmentWithID id: UUID) {
    guard let panelIndex = adornmentPanelMapping[id] else { return }
    activeImageGenerationPanelIndex = panelIndex

    let imagePlaygroundViewController = ImagePlaygroundViewController()
    imagePlaygroundViewController.delegate = self
    present(imagePlaygroundViewController, animated: true)
}

func imageViewController(_ imageViewController: ImagePlaygroundViewController,
                         didCreateImageAt imageURL: URL) {
    guard let panelFrame = activeGenerationPanelFrame,
          let paperMarkupViewController = pageViewController.paperViewController,
          var markup = paperMarkupViewController.markup,
          let image = UIImage(contentsOfFile: imageURL.path) else { return }

    let imageMarkup = ImageMarkup(frame: panelFrame, image: image)
    markup.subelements.append(imageMarkup)
    paperMarkupViewController.markup = markup
}

Resources

Unofficial, not associated with Apple. BySuperwall

On this page

Ask AI