Unwrap PaperKit
Use PaperKit's new data model, markup elements, and adornments to build editable canvas apps with locked templates and custom overlays.
TL;DR
- PaperMarkup now exposes
subelementsas a readable and writableMarkupOrderedSet, letting apps create, inspect, reorder, and update canvas contents programmatically. - Every canvas item conforms to
Markup;allowedInteractionslets apps selectively permit moving, resizing, rotating, deleting, styling, or selecting, with.readOnlyfor 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.
MarkupAdornmentadds 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
PaperMarkupas the document-level canvas model. - Use
PaperMarkupViewController.markupto 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
PaperMarkupwith bounds matching the page size. - Read
markup.subelementsinto a mutableMarkupOrderedSet. - 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
allowedInteractionson individual elements, not just the whole canvas. - Set template elements to
.readOnlywhen 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.subelementsto inspect and modify existing canvas elements. - Cast to concrete types such as
ShapeMarkuporImageMarkupwhen type-specific properties are needed. - Use
updateOrAppend(_:)after changing an element in aMarkupOrderedSet. - 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
Related Sessions
- Create high quality images using Image Playground
- Read between the strokes with PencilKit
- Meet PaperKit
Code-along: Add persistence with SwiftData
Add SwiftData persistence to an existing SwiftUI app by converting state to @Model schemas, relationships, model containers, and targeted @Query fetches.
SwiftData Group Lab
Online WWDC26 group lab for discussing SwiftData announcements and getting guidance from Apple engineers and designers.