クリップボード履歴と保存スナップショットを分離する永続化設計
HistoryとSavedItemを別ライフサイクルにし、論理削除・Undo・asset ownershipまで分離したiOS実装。
History rowを保存物のsource of truthにしない
Copy MemoryではHistoryとStockを別の永続ライフサイクルとして扱います。Stock側のSavedItemはHistory rowへの参照ではなく、text、kind、assetFileName、typeIdentifier、displayNameなどを持つ独立snapshotです。
これによりHistoryを削除してもSavedItemは残ります。History IDをforeign keyとして保持する構成にすると、履歴整理と保存物の保持期間が結合し、削除・復元・asset cleanupの責務が複雑になります。
Stockの階層はSavedSection -> SavedItemだけ
SavedSectionはrole、title、icon、tint、isCollapsed、itemsを持ちます。roleはpinned / regularの2値で、先頭の「ピン留め」はtitle文字列ではなくroleで識別します。
nested folderやroot itemを持たせず、すべてのSavedItemが一つのSavedSectionへ属する形に固定しています。UIの単純化だけでなく、reorder、move、delete、restore時の座標をsection ID + item indexで一意に扱いやすくするためです。
History削除とStock削除でUndo方式を分ける
HistoryはClipRepository.softDeleteでdeletedAtを付与する論理削除です。通常検索からは除外しつつ、削除済み画面とUndoから復元できます。複数選択削除も一つのtransactionとして扱い、undoHistoryDeletionでまとめて戻します。
StockはSavedItem自体を即座に一覧から外しますが、画像・ファイルassetを持つため、削除前にSavedRepository.stageAssetsForUndoでsession-scopedなtemporary areaへ退避します。metadataだけ戻せてもassetが消えていればUndoにならないためです。
Undo中のfilename再利用を競合として扱う
Stock itemを削除してUndoするまでの間に、別itemが同じvisible filenameを取得する可能性があります。その場合、元filenameへ上書き復元すると別データを破壊します。
SavedRepositoryのrestore側ではfilenameが再利用されていた場合にunique filenameへ変更して復元します。UI上の「元に戻す」は単純なarray reinsertionではなく、metadata、section/order、owned assetの整合性をまとめて戻す処理です。
selection操作もidentityを壊す操作と壊さない操作に分ける
複数選択の「セクション整理」は各SavedItemのidentityを維持してmembershipだけを変えます。一方「テキスト結合」は複数itemの境界を破棄し、一つのtext itemを生成します。
両者を同じAPIや曖昧な「まとめる」操作へ寄せると、undo semanticsとasset ownershipが不明確になります。保存構造の変更とcontent transformationを別commandとして扱うことで、domain上の副作用を限定しています。