ハイライト
iPadOS 17 では、ドキュメント App のコンテンツビューコントローラー基盤として UIDocumentViewController が導入されました。共有、ドラッグ&ドロップ、取り消し/やり直し、自動リネームなどが自動で提供され、開発者はドキュメント管理のボイラープレートから解放され、App 固有の機能に集中できます。
主要内容
ドキュメント App の3層分類
ドキュメント App は3種類に分かれます。ブラウザ型(Files など)、ビューア型(Quick Look など)、エディタ型(Pages など)です。今回の更新は主にビューアとエディタ向けですが、ブラウザにも恩恵があります。
UIDocument:ドキュメントモデルの中核
UIDocument はすべてのドキュメント App の基盤です。抽象基底クラスで、サポートする各ファイルタイプごとにサブクラスを作成します。すべての UIDocument は URL ベースです。ディスク上のファイルでも、カスタム URL スキームのデータベースでも構いません。
読み込みと保存は非同期です。UIDocument が内部でスレッドセーフとファイル調整を処理します。開発者が気にするのは2点だけです。読み込み/保存の実装と、ドキュメントコンテンツへのアクセス方法です。
単純なファイル型ドキュメントなら、2つの便利メソッドをオーバーライドするだけで十分です。load(fromContents:ofType:) は開くとき、contents(forType:) は保存するときに呼ばれます。完全な制御が必要な場合(データベースへの保存など)は、save(to:for:completionHandler:) と read(from:) をオーバーライドします。
コンテンツアクセスと自動保存
UIDocument サブクラスにプロパティを追加してドキュメントコンテンツを公開します。プロパティが変わるたびに updateChangeCount(.done) を呼びます。UIDocument は保存が必要だと判断し、適切なタイミングで自動保存をトリガーします。
UIDocumentViewController:自動化されたドキュメント表示
iPadOS 17 の新しい基底クラスで、UIDocument と連携します。自動で処理する内容は次のとおりです。
- ナビゲーションバーのタイトルとタイトルメニュー
- ドキュメント共有
- ドキュメントのドラッグ&ドロップ
- 取り消し/やり直しショートカット
- 自動リネーム
モジュラー設計で、システムが合理的なデフォルトを提供し、各動作を個別にカスタマイズできます。
主要なコールバックメソッド
documentDidOpen() はドキュメントを開いた後に呼ばれ、ビューをドキュメントコンテンツ用に構成します。navigationItemDidUpdate() はナビゲーション項目の更新後に呼ばれ、カスタムナビゲーションボタンを追加します。
これらのコールバックの呼び出し順序は保証されません。documentDidOpen() と viewDidLoad() の両方からビュー構成を呼び出し、isViewLoaded とドキュメント状態を確認してください。
取り消し/やり直し
システム提供の undoRedoItemGroup をナビゲーションバーに配置します。ドキュメントに undoManager を割り当てれば、UIDocumentViewController が表示/非表示とボタンの有効/無効を自動管理します。
自動リネーム
iPadOS 17 では UIDocument が UINavigationItemRenameDelegate に準拠し、リネーム時のファイル操作を自動処理します。UIDocumentViewController 使用時は自動構成されます。使わない場合は navigationItem.renameDelegate = document を手動設定します。
ブラウザがない場合のフォールバック
UIDocumentViewController がルートビューコントローラーでブラウザ層がない場合、ナビゲーションバーにドキュメントボタンを自動配置し、タップでドキュメントピッカーを開きます。Info.plist に UIDocumentClass キーを宣言し、対応する UIDocument サブクラスを指定します。
移行の3ステップ
- コンテンツビューコントローラーの基底クラスを UIDocumentViewController に変更
- 既存コードを新しいコールバック(
documentDidOpen()、navigationItemDidUpdate())へ移動 - 不要になったコード(リネームデリゲート、ナビゲーション項目構成、ドキュメントプロパティ更新など)を削除
詳細
ドキュメントの読み込み
(03:54)
override func load(fromContents contents: Any, ofType typeName: String?) throws {
// Load your document from contents
guard let data = contents as? Data,
let text = String(data: data, encoding: .utf8) else {
throw DocumentError.readError
}
self.text = text
}
キーポイント:
load(fromContents:ofType:)はドキュメントを開くときに呼ばれるcontentsは通常ファイルでは Data、パッケージファイルでは FileWrapper- 解析失敗時はエラーをスロー。UIDocument がエラー状態を処理する
ドキュメントの保存
(04:08)
override func contents(forType typeName: String) throws -> Any {
// Encode your document with an instance of NSData or NSFileWrapper
guard let data = self.text?.data(using: .utf8) else {
throw DocumentError.writeError
}
return data
}
キーポイント:
contents(forType:)は保存時に呼ばれる- Data または FileWrapper を返す
- UIDocument が非同期保存とファイル調整を自動処理する
手動の保存と読み込み
(04:34)
override func save(to url: URL,
for saveOperation: UIDocument.SaveOperation,
completionHandler: ((Bool) -> Void)? = nil) {
self.performAsynchronousFileAccess {
// Set up file coordination and write file to URL
}
}
override func read(from url: URL) throws {
// Set up file coordination and read file from URL
}
キーポイント:
- 読み書きを完全に制御する場合にこのペアを使う
save(to:for:completionHandler:)は非同期read(from:)は同期。戻る前に読み込みを完了するperformAsynchronousFileAccessでファイルアクセスのスレッドセーフを確保
ドキュメントの変更をマーク
(05:08)
class Document: UIDocument {
var text: String? {
didSet {
if oldValue != nil && oldValue != text {
self.updateChangeCount(.done)
}
}
}
}
キーポイント:
updateChangeCount(.done)でコンテンツ変更を UIDocument に通知- 初回読み込み時は
oldValueが nil のため誤マークを防げる - UIDocument が適切なタイミングで自動保存する
ドキュメントビューの構成
(06:30)
override func documentDidOpen() {
configureViewForCurrentDocument()
}
override func viewDidLoad() {
super.viewDidLoad()
configureViewForCurrentDocument()
}
func configureViewForCurrentDocument() {
guard let document = markdownDocument,
!document.documentState.contains(.closed)
&& isViewLoaded else { return }
// Configure views for document
}
キーポイント:
documentDidOpen()とviewDidLoad()の両方から構成メソッドを呼ぶisViewLoadedとドキュメント状態を確認し、両方が準備できていることを保証- コールバック順序を前提にしない防御的プログラミング
ナビゲーション項目の更新
(07:17)
override func navigationItemDidUpdate() {
// Customize navigation item
}
キーポイント:
- UIDocumentViewController がナビゲーション項目を更新した後に呼ばれる
- ここでカスタムボタンを追加したりナビゲーションバーを変更する
- システムは変更をできるだけ保持しようとする
ドキュメントの手動オープン
(08:01)
documentController.openDocument { success in
if success {
self.present(documentController, animated: true)
}
}
キーポイント:
- 外部からドキュメントを開く必要があるときに手動で呼ぶ
documentDidOpenなどのコールバックを自動処理- 完了後は completion handler で present する
リネームデリゲート
(09:20)
navigationItem.renameDelegate = document
キーポイント:
- UIDocumentViewController を使わない場合は手動設定
- UIDocument がリネーム時のファイル操作を自動処理
- UIDocumentViewController 使用時は自動構成
重要ポイント
1. 既存のドキュメント App を UIDocumentViewController に移行
- 何を作るか: コンテンツビューコントローラーの基底クラスを UIViewController から UIDocumentViewController に変更
- 価値がある理由: 共有、ドラッグ&ドロップ、取り消し/やり直し、リネームなどを自動取得し、大量のボイラープレートを削除できる
- 始め方: 基底クラス変更 → 新コールバックへコード移動 → 不要なナビゲーション項目構成とリネームデリゲートを削除
2. ドキュメントモデルに自動保存を追加
- 何を作るか: ドキュメントプロパティの
didSetでupdateChangeCount(.done)を呼ぶ - 価値がある理由: UIDocument が保存タイミングを処理。ユーザーは手動で保存する必要がない
- 始め方: ドキュメントコンテンツを変更するすべての箇所を見つけ、各変更で
updateChangeCountを呼ぶ
3. undoRedoItemGroup で標準的な取り消し/やり直しを提供
- 何を作るか:
undoRedoItemGroupをナビゲーションバーに配置し、ドキュメントに undoManager を割り当てる - 価値がある理由: システムがボタンの表示/非表示と有効/無効を管理。ゼロコードで標準体験
- 始め方: UndoManager を作成して
document.undoManagerに割り当て、toolbar items にundoRedoItemGroupを追加
4. Markdown エディタを実装
- 何を作るか: UIDocument + UIDocumentViewController でデスクトップクラス iPad 体験の Markdown エディタを作る
- 価値がある理由: 自動保存、共有、リネーム、ドラッグ&ドロップ、空状態など一式を取得し、エディタ本体に集中できる
- 始め方: テキスト読み書き用の MarkdownDocument サブクラスと、編集 UI 用の UIDocumentViewController サブクラスを作成
5. ブラウザなしの単一ドキュメント App にドキュメントピッカーを追加
- 何を作るか: 単一ドキュメント App でもファイルシステムからドキュメントを開けるようにする
- 価値がある理由: ルートの UIDocumentViewController がドキュメントボタンを自動表示
- 始め方: Info.plist に
UIDocumentClassキーを宣言し、UIDocument サブクラスを指定
関連セッション
- What’s new in UIKit — UIKit の新機能。UIDocumentViewController の概要を含む
- Meet desktop-class iPad — デスクトップクラス iPad App の設計思想
- Build a desktop-class iPad app — デスクトップクラス iPad App 構築の実践ガイド
コメント
GitHub Issues · utterances