Highlight
PaperKit は Apple 自身のマークアップフレームワークで、これまでは Notes、スクリーンショット、QuickLook、Journal といったシステム App 内でしか使われていませんでした。今回からサードパーティ App でも直接組み込めるようになります。
核となる内容
これまで自分の App で Notes やスクリーンショットの注釈のような「ペンで描く + 図形を追加 + 画像をドラッグ + テキストボックス」といった体験を実現しようとすると、ほぼゼロから車輪を再発明する必要がありました。レイヤーを自前で管理し、PencilKit の描画とマークアップ要素の混在レイアウトを処理し、挿入メニューを設計し、macOS 向けに調整する。コア機能とは言えない部分に、しばしば数千行のコードを書く羽目になっていました。
WWDC25 で Apple は、自社で長年使ってきた PaperKit フレームワークを外部に開放しました(00:07)。これは Apple のシステムレベルのマークアップ体験を支える基盤であり、Notes、Screenshots、QuickLook、Journal はすべてこの上で動いています。今回、macOS Tahoe でもネイティブ対応となりました(00:57)。
フレームワークは 3 つのコアコンポーネントで構成されています。PaperMarkupViewController はインタラクティブなキャンバスで、マークアップと PencilKit 描画の表示・編集を担当します。PaperMarkup はデータモデルのコンテナで、保存・読み込み・レンダリングを担います。そして MarkupEditViewController(iOS / iPadOS / visionOS)と MarkupToolbarViewController(macOS)が、図形・画像・テキストボックスなどの要素挿入を担当します(02:31)。この 3 つを組み合わせるだけで、システム App と同じ注釈機能が手に入ります。
詳細な解説
iOS での最小構成は 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(_:)))
}
ポイントは次のとおりです。
PaperMarkup(bounds:)でデータモデルを生成し、view.boundsを渡してレンダリングコンテキストを正しく確保します。PaperMarkupViewController(markup:supportedFeatureSet:)でキャンバスを生成し、.latestを渡せば現時点のすべてのマークアップ機能を取得できます。addChild+didMove(toParent:)は、UIKit の標準的な container view controller の組み込み方です。PKToolPickerはaddObserverでキャンバスを observer として登録することで、ツール切り替え時にキャンバスが自動的に反応します。pencilKitResponderState.activeToolPickerは UIResponder 上の新しい API で、現在のレスポンダに紐づくツールピッカーを指定します(04:32)。toolPickerVisibilityは表示状態を制御します。非表示にしてもダブルタップやスクイーズジェスチャは引き続き有効なので、ミニマルなツールピッカー UI を実現するのにも便利です(05:03)。toolPicker.accessoryItemでツールピッカーに+ボタンを差し込み、挿入メニューのエントリポイントとして利用します。
macOS でも構造はほぼ同じで、違いは挿入用 UI がポップアップメニューではなくツールバーになる点だけです(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()
}
ポイントは次のとおりです。
- macOS では iOS のポップアップ型
MarkupEditViewControllerの代わりにMarkupToolbarViewControllerを使います。 toolbarViewController.delegate = paperViewControllerとすることで、ツールバーの挿入コールバックがキャンバスに繋がります。- キャンバスとツールバーで同じ
FeatureSetを共有することで、利用可能なツールを揃えます。
データの保存は delegate コールバックで行います(07:18)。
func paperMarkupViewControllerDidChangeMarkup(_ paperMarkupViewController: PaperMarkupViewController) {
let markupModel = paperMarkupViewController.markup
Task {
let data = try await markupModel.dataRepresentation()
try data.write(toFile: paperKitDataURL)
}
}
ポイントは次のとおりです。
paperMarkupViewControllerDidChangeMarkupはキャンバスの変更があるたびに呼ばれます。markupModel.dataRepresentation()は async メソッドで、Dataを返すのでそのままディスクに書き出せます。PaperMarkupViewControllerはObservableにも適合しているため、状態駆動が好みであれば delegate の代わりに利用できます(07:33)。
forward compatibility(前方互換性)の観点から、サムネイルを保存しておきバージョン不一致時に表示するのが推奨されています(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()
}
ポイントは次のとおりです。
- 自前で
CGContextを生成して描画ターゲットとします。 markupModel.draw(in:frame:)は async メソッドで、現在のモデルを context に描画します。- 生成した
CGImageをマークアップデータと一緒に保存しておくのが、Notes などのシステム App でも採られている手法です。
ツールセットのカスタマイズは「.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)
ポイントは次のとおりです。
FeatureSet.latestにはフレームワークの現時点でのすべての機能が含まれており、将来追加される機能も自動で取り込めます。remove/insertで個別の機能を増減できます。たとえばテキストボックスを除外して、代わりに sticker を追加するといった具合です。colorMaximumLinearExposureを 1 より大きくすると HDR マークアップが有効になります。キャンバスとツールピッカーの両方に設定する必要があります(09:30)。- 推奨値 4 はセッションのサンプル App で使われている値です。
UIScreen/NSScreenから実際の headroom を読み取るのも選択肢です。 - HDR を有効化すると、PencilKit に新しく追加された筆ペン(Reed tool)も利用できるようになります(10:22)。
よく使う最後の拡張ポイントは、背景のカスタマイズです(10:50)。
let template = UIImage(named: "MyTemplate.jpg")
let templateView = UIImageView(image: template)
paperViewController.contentView = templateView
ポイントは次のとおりです。
contentViewは任意のUIViewを受け取り、その上にすべてのマークアップと描画がレンダリングされます。- 罫線・ドット・レシピカードのようなテンプレート用紙、つまり「下地は固定、上にだけ書き込む」用途にぴったりです。
SwiftUI からは UIViewControllerRepresentable で包めば組み込めます(06:46)。
押さえておきたいポイント
1. PaperKit でノート / リーダー / 教育系 App に「Apple 純正クオリティ」の注釈機能をワンタッチで追加する
なぜやる価値があるか: これまで手書き + 図形 + 画像が混在するマークアップを自前実装するのは膨大な工数でしたが、PaperKit を使えば数十行のコードで Notes / Journal と同等の体験を再利用できます。
どう始めるか: まずは iOS で最小構成(PaperMarkup + PaperMarkupViewController + PKToolPicker)を動かし、そこから delegate コールバックで自分のストレージにデータを永続化する流れがおすすめです。
2. FeatureSet.latest を起点にした「引き算」で構築する
なぜやる価値があるか: .latest はフレームワークのアップデートに自動追従するため、将来追加される新ツールもコード変更なしで取り込めます。空集合に insert していく方式だと新機能を見落としやすく、メンテナンスコストが膨らみがちです。
どう始めるか: var featureSet: FeatureSet = .latest と宣言し、自分の App に不要なツールだけ remove で外しましょう。たとえば純粋な閲覧用注釈アプリなら remove(.stickers) といった具合です。
3. サムネイル保存で前方互換性を担保し、新バージョンのデータが旧バージョンでクラッシュ・空表示にならないようにする
なぜやる価値があるか: ユーザーが iOS 27 で編集したファイルを iOS 26 で開く、ということは普通に起こり得ます。新しい要素が解析できなくても、ディスクにサムネイルがあれば旧バージョンでも内容は確認できます。
どう始めるか: delegate コールバックの中で markupModel.dataRepresentation() とサムネイル描画を併せて実行し、両方をセットで保存します。読み込み時にバージョンを確認し、一致しない場合はサムネイルを表示します。
4. pencilKitResponderState.toolPickerVisibility を活用してミニマルなツールピッカーを作る
なぜやる価値があるか: ツールピッカーを非表示にしてもダブルタップ / スクイーズジェスチャは利用できるので、操作性を維持したままよりコンパクトなカスタムツールバー UI を作れます。
どう始めるか: ツールピッカーを非表示にして、自前で簡易なツールボタンを描画します。ボタンから PKToolPicker の状態 API を呼び出してツールを切り替えるようにします。
関連セッション
- Squeeze the most out of Apple Pencil — Apple Pencil のスクイーズジェスチャとツールピッカーのインタラクションに関する公式ガイド
- Introducing PencilKit — PencilKit のキャンバスとツールピッカーの入門
- Dive deeper into Writing Tools — Writing Tools をテキストコントロール内で直接校正・書き換え・変換に使う
- Enhance child safety with PermissionKit — PermissionKit を使ってコミュニケーション系 App での子どものやり取りをより安全に
- Discover Apple-Hosted Background Assets — Apple がホストする Background Assets の新機能
コメント
GitHub Issues · utterances