Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Beta-sensitive. PaperKit is new in iOS/iPadOS 26, macOS 26, and visionOS 26. Verify all patterns against current Apple documentation before shipping.
Extended patterns, data persistence strategies, rendering, multi-platform considerations, and advanced FeatureSet usage for PaperKit.
PaperMarkup.dataRepresentation() is async. Always call from an async context and handle errors.
import PaperKit
actor MarkupStore {
private let fileURL: URL
init(directory: URL, filename: String = "markup.paperkit") {
self.fileURL = directory.appendingPathComponent(filename)
}
func save(_ markup: PaperMarkup) async throws {
let data = try await markup.dataRepresentation()
try data.write(to: fileURL, options: .atomic)
}
func load() throws -> PaperMarkup {
let data = try Data(contentsOf: fileURL)
return try PaperMarkup(dataRepresentation: data)
}
func loadOrCreate(bounds: CGRect) -> PaperMarkup {
do {
return try load()
} catch {
return PaperMarkup(bounds: bounds)
}
}
}Store a rendered thumbnail next to the data file for use in file browsers or version-mismatch fallback. This is the pattern used by Notes.
func saveWithThumbnail(
_ markup: PaperMarkup,
dataURL: URL,
thumbnailURL: URL,
thumbnailSize: CGSize
) async throws {
// Save data
let data = try await markup.dataRepresentation()
try data.write(to: dataURL, options: .atomic)
// Render and save thumbnail
let renderer = UIGraphicsImageRenderer(size: thumbnailSize)
let options = RenderingOptions(traitCollection: .current)
let image = renderer.image { ctx in
let frame = CGRect(origin: .zero, size: thumbnailSize)
Task {
await markup.draw(in: ctx.cgContext, frame: frame, options: options)
}
}
if let pngData = image.pngData() {
try pngData.write(to: thumbnailURL, options: .atomic)
}
}PaperMarkupViewController conforms to Observable. Use Observation framework tracking or the delegate for auto-save.
let markups = Observations.untilFinished { [weak paperVC] in
if let markup = paperVC?.markup {
return .next(markup)
}
return .finish
}
Task { [weak self] in
for await newMarkup in markups {
try? await self?.store.save(newMarkup)
}
}Avoid saving on every stroke. Debounce saves to reduce disk I/O:
class MarkupViewController: UIViewController, PaperMarkupViewController.Delegate {
var paperVC: PaperMarkupViewController!
private var saveTask: Task<Void, Never>?
func paperMarkupViewControllerDidChangeMarkup(
_ controller: PaperMarkupViewController
) {
saveTask?.cancel()
saveTask = Task {
try? await Task.sleep(for: .seconds(1))
guard !Task.isCancelled else { return }
guard let markup = controller.markup else { return }
try? await store.save(markup)
}
}
}When loading markup data created by a newer version of the app or OS, the data may contain features unsupported by the current FeatureSet. Handle this gracefully.
func loadAndValidate(
from url: URL,
supportedFeatures: FeatureSet
) throws -> LoadResult {
let data = try Data(contentsOf: url)
let markup = try PaperMarkup(dataRepresentation: data)
if markup.featureSet.isSubset(of: supportedFeatures) {
return .editable(markup)
} else {
return .readOnly(markup)
}
}
enum LoadResult {
case editable(PaperMarkup)
case readOnly(PaperMarkup)
}If the loaded markup uses features the current app version does not support, show a pre-rendered thumbnail instead of a broken editor. This matches the Notes app behavior.
func handleVersionMismatch(
markup: PaperMarkup,
in view: UIImageView,
size: CGSize
) async {
let options = RenderingOptions(traitCollection: .current)
let renderer = UIGraphicsImageRenderer(size: size)
let thumbnail = renderer.image { ctx in
let frame = CGRect(origin: .zero, size: size)
Task {
await markup.draw(in: ctx.cgContext, frame: frame, options: options)
}
}
view.image = thumbnail
}Alternatively, remove unsupported elements and allow editing of the rest:
var markup = try PaperMarkup(dataRepresentation: data)
markup.removeContentUnsupported(by: appFeatureSet)
paperVC.markup = markupThis mutates the model in place, dropping any elements not representable by the given FeatureSet.
func render(
markup: PaperMarkup,
size: CGSize,
darkMode: Bool = false
) async -> CGImage? {
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let context = CGContext(
data: nil,
width: Int(size.width),
height: Int(size.height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else { return nil }
let options = RenderingOptions(
darkUserInterfaceStyle: darkMode,
layoutRightToLeft: false
)
await markup.draw(
in: context,
frame: CGRect(origin: .zero, size: size),
options: options
)
return context.makeImage()
}Use the current environment's traits for correct appearance:
let options = RenderingOptions(traitCollection: .current)Use contentsRenderFrame to render only the area that has content:
let contentFrame = markup.contentsRenderFrame
let size = contentFrame.size
// Render just the content area
await markup.draw(
in: context,
frame: CGRect(origin: .zero, size: size),
options: options
)#if os(iOS) || os(visionOS)
import PaperKit
func setupInsertionUI(
features: FeatureSet,
markupVC: PaperMarkupViewController
) -> UIViewController {
let editVC = MarkupEditViewController(
supportedFeatureSet: features,
additionalActions: []
)
editVC.delegate = markupVC
return editVC
}
#endif
#if os(macOS)
import PaperKit
func setupInsertionUI(
features: FeatureSet,
markupVC: PaperMarkupViewController
) -> NSViewController {
let toolbar = MarkupToolbarViewController(supportedFeatureSet: features)
toolbar.delegate = markupVC
return toolbar
}
#endifMarkupToolbarViewController exposes additional state:
| Property | Type | Description |
|---|---|---|
selectedDrawingTool | any PKTool | Active drawing tool |
selectedDrawingToolItem | PKToolPickerItem | Active tool picker item |
selectedIndirectPointerTouchMode | TouchMode | Current pointer mode |
indirectPointerTouchModes | [TouchMode] | Available pointer modes |
Complete setup matching the WWDC25 session pattern:
import PaperKit
import PencilKit
import UIKit
class RecipeMarkupViewController: UIViewController, PaperMarkupViewController.Delegate {
var paperVC: PaperMarkupViewController!
var toolPicker: PKToolPicker!
let store: MarkupStore
init(store: MarkupStore) {
self.store = store
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
let markup = store.loadOrCreate(bounds: view.bounds)
let features = FeatureSet.latest
// Markup controller
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)
// Tool picker
toolPicker = PKToolPicker()
toolPicker.addObserver(paperVC)
paperVC.pencilKitResponderState.activeToolPicker = toolPicker
paperVC.pencilKitResponderState.toolPickerVisibility = .visible
// Insertion menu button in tool picker accessory
let insertButton = UIBarButtonItem(
systemItem: .add,
primaryAction: UIAction { [weak self] _ in
self?.presentInsertionMenu()
}
)
toolPicker.accessoryItem = insertButton
}
func presentInsertionMenu() {
let editVC = MarkupEditViewController(
supportedFeatureSet: paperVC.supportedFeatureSet,
additionalActions: []
)
editVC.delegate = paperVC
editVC.modalPresentationStyle = .popover
if let popover = editVC.popoverPresentationController {
popover.barButtonItem = toolPicker.accessoryItem
}
present(editVC, animated: true)
}
// MARK: - Delegate
func paperMarkupViewControllerDidChangeMarkup(
_ controller: PaperMarkupViewController
) {
guard let markup = controller.markup else { return }
Task { try? await store.save(markup) }
}
}import PaperKit
import PencilKit
import AppKit
class MacMarkupViewController: NSViewController {
var paperVC: PaperMarkupViewController!
let features = FeatureSet.latest
override func viewDidLoad() {
super.viewDidLoad()
let markup = PaperMarkup(bounds: view.bounds)
paperVC = PaperMarkupViewController(
markup: markup,
supportedFeatureSet: features
)
addChild(paperVC)
paperVC.view.frame = view.bounds
paperVC.view.autoresizingMask = [.width, .height]
view.addSubview(paperVC.view)
// macOS toolbar for insertion UI
let toolbar = MarkupToolbarViewController(supportedFeatureSet: features)
toolbar.delegate = paperVC
addChild(toolbar)
// Position toolbar at top of view
let toolbarHeight: CGFloat = 44
toolbar.view.frame = CGRect(
x: 0, y: view.bounds.height - toolbarHeight,
width: view.bounds.width, height: toolbarHeight
)
toolbar.view.autoresizingMask = [.width, .minYMargin]
view.addSubview(toolbar.view)
}
}For apps that need markup annotations without freeform drawing:
var features = FeatureSet.latest
features.remove(.drawing)
// User can insert shapes, text, images but cannot draw freehandvar features = FeatureSet.empty
features.insert(.shapeStrokes)
features.insert(.shapeFills)
features.shapes = [.rectangle, .ellipse, .arrowShape]var features = FeatureSet.latest
features.remove(.stickers)
features.remove(.images)
features.shapes = [.rectangle, .ellipse, .line, .arrowShape]
features.lineMarkerPositions = .single // Single-ended arrows onlyvar features = FeatureSet.latest
features.colorMaximumLinearExposure = view.window?.windowScene?.screen.potentialEDRHeadroom ?? 1.0
// Also set on tool picker:
toolPicker.maximumLinearExposure = features.colorMaximumLinearExposureBuild markup content in code without user interaction — useful for generating templates, reports, or test content.
func createAnnotatedTemplate(size: CGSize) -> PaperMarkup {
var markup = PaperMarkup(bounds: CGRect(origin: .zero, size: size))
// Title text box
markup.insertNewTextbox(
attributedText: AttributedString("Document Title"),
frame: CGRect(x: 20, y: 20, width: size.width - 40, height: 40),
rotation: 0
)
// Decorative line separator
let lineConfig = ShapeConfiguration(
type: .line,
fillColor: nil,
strokeColor: UIColor.separator.cgColor,
lineWidth: 1
)
markup.insertNewLine(
configuration: lineConfig,
from: CGPoint(x: 20, y: 70),
to: CGPoint(x: size.width - 20, y: 70),
startMarker: false,
endMarker: false
)
// Annotation callout
let calloutConfig = ShapeConfiguration(
type: .chatBubble,
fillColor: UIColor.systemYellow.withAlphaComponent(0.2).cgColor,
strokeColor: UIColor.systemYellow.cgColor,
lineWidth: 1.5
)
markup.insertNewShape(
configuration: calloutConfig,
frame: CGRect(x: 20, y: 90, width: 280, height: 80),
rotation: 0
)
return markup
}var combined = PaperMarkup(bounds: totalBounds)
combined.append(contentsOf: page1Markup)
combined.append(contentsOf: page2Markup)Apply affine transforms to all content in a markup model:
// Scale content to 50%
let scale = CGAffineTransform(scaleX: 0.5, y: 0.5)
markup.transformContent(scale)
// Translate content
let translate = CGAffineTransform(translationX: 100, y: 50)
markup.transformContent(translate)
// Combined transform
let transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
.translatedBy(x: 100, y: 50)
markup.transformContent(transform)For apps already using PencilKit that want to adopt PaperKit:
import PencilKit
func migrateDrawing(_ drawing: PKDrawing, to markup: inout PaperMarkup) {
markup.append(contentsOf: drawing)
}PaperKit handles both layers internally. The drawingTool property accepts any PKTool:
paperVC.drawingTool = PKInkingTool(.pen, color: .black, width: 3)
paperVC.drawingTool = PKEraserTool(.bitmap)
paperVC.drawingTool = PKLassoTool()MarkupError covers deserialization failures:
| Case | Meaning |
|---|---|
.incorrectFormat | Data is not PaperKit format |
.malformedData | Data is corrupted |
.incompatibleFormatTooNew | Data requires a newer PaperKit version |
do {
let markup = try PaperMarkup(dataRepresentation: data)
paperVC.markup = markup
} catch MarkupError.incompatibleFormatTooNew {
// Show thumbnail fallback or upgrade prompt
showUpgradePrompt()
} catch MarkupError.malformedData {
// Data corrupted — offer to start fresh
showCorruptionAlert()
} catch MarkupError.incorrectFormat {
// Not PaperKit data
showFormatError()
} catch {
showGenericError(error)
}PaperMarkupViewController exposes an undoManager property. The controller registers undo actions automatically for user interactions. Connect it to the responder chain for standard undo/redo behavior.
// The undoManager is available after viewDidLoad
override var undoManager: UndoManager? {
paperVC.undoManager
}PaperKit fits naturally into UIDocument subclasses:
class MarkupDocument: UIDocument {
var markup: PaperMarkup?
override func contents(forType typeName: String) throws -> Any {
guard let markup else { throw CocoaError(.fileWriteUnknown) }
// Use a synchronous wrapper or pre-computed data
// Note: dataRepresentation() is async — pre-compute before save
return precomputedData ?? Data()
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let data = contents as? Data else {
throw CocoaError(.fileReadCorruptFile)
}
markup = try PaperMarkup(dataRepresentation: data)
}
}Since dataRepresentation() is async, pre-compute the data representation before the document system calls contents(forType:). Trigger serialization in the delegate callback when markup changes.
PaperMarkup.indexableContent returns extractable text from text boxes, useful for Spotlight indexing:
if let searchText = markup.indexableContent {
// Index with Core Spotlight
let attributes = CSSearchableItemAttributeSet(contentType: .data)
attributes.textContent = searchText
}skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references