【Swift】.registerUndoとは?意味や使い方をわかりやすく解説!

「さっきの変更を取り消したい」「やっぱり元に戻したい」。その“戻すための処理”をあらかじめ登録しておくのが .registerUndo です。

SwiftUI では @Environment(\.undoManager) から取得した UndoManager に対して呼び出し、取り消すときに実行される逆操作を記録します。

登録のコツさえつかめば、Redo(やり直し)まで自然に動かせます。

.registerUndoとは?

.registerUndo(withTarget:_:)UndoManagerのメソッドで、取り消し時に呼ぶ処理を登録します。

“箱に逆再生テープを入れておく”イメージです。

ユーザーが「元に戻す」を実行すると、そのテープが再生されて状態が巻き戻ります。

  • ターゲットは class(参照型)必須UndoManager はターゲットを weak参照 するため、struct(例:SwiftUIの View)は渡せません。class な ViewModel や小さなプロキシをターゲットにします。
  • Redoを効かせるコツUndo処理の中で“元の操作”をもう一度登録します。Undo ↔ Redo を往復できるようになります。

使い方

基本手順は次のとおりです。

  1. UndoManagerを用意(SwiftUIなら @Environment(\.undoManager))。
  2. 安定したターゲット(class)を決める(ViewModel など)。
  3. 変更前の値を保持し、.registerUndo(withTarget:) の中で戻す処理を書く。
  4. 実際の変更を行い、必要なら .setActionName(_:) で表示名を付ける。
  5. Undoハンドラの中で元の操作を再登録して、Redoまで成立させる。

最小コード(ViewModel方式)です。

.registerUndowithTarget: には、取り消し時に“呼び戻したい相手”=上のコードでは withTarget: self を渡しています。

つまり CounterModel の同一インスタンスが「呼び戻したい相手(ターゲット)」です。

上記コードの undo?.registerUndo(withTarget: self) { me in
me.setCount(old) // ← Undo時に再登録 → Redoも自然に成立
}
部分がだいぶわかりにくいと思うので下記解説します。

何が起きているか(時系列)

  1. setCount(_:) を呼ぶとき、まず 「元の値へ戻す処理」registerUndo で登録します。
    具体的には「あとで Undo されたら setCount(old) を呼んでね」と箱に入れておきます。
  2. そのあとで実際に count = newValue に更新します。
  3. ユーザーが Undo を押すと、箱の中身が実行されて setCount(old) が呼ばれます
  4. ここがポイント:setCount は「呼ばれるたびに逆操作を登録する」メソッドなので、Undo 中に呼ばれた setCount(old) も “逆方向” を登録します
    つまり「次に Redo されたら setCount(newValue) を呼んでね」が箱に入る。これが Redo 用の登録 です。
  5. だから Undo の直後に Redo を押すと、今度は箱から setCount(newValue) が実行され、元の値に戻せます。

具体例

  • 初回 setCount(1)
    → Undoスタックに「setCount(0)」を登録 → count = 1
  • Undo
    setCount(0) が実行される
    → その中で 逆方向(Redo用)として「setCount(1)」を登録 → count = 0
  • Redo
    → 登録されていた「setCount(1)」が実行 → また setCount 内で逆方向(setCount(0))が登録 → count = 1

つまり

  • me.setCount(old) は「元に戻す」実行そのもの。
  • その 実行の中setCount がいつもどおり “逆操作の登録” を行うので、Redo が自動で成立します。
  • もし setCount 内で逆操作を登録していなければ、Undo はできても Redo は効きません

この「操作するたびに、逆操作を登録する」という性質のおかげで、Undo から Redo へ自然に往復できるようになっています。

具体例

ここでは実務で使う頻度が高いパターンを3つ紹介します。コードのあとに使いどころを説明します。

1. SwiftData の変更を Undo 対象にする

ModelContextundoManager を割り当てると、挿入・更新・削除が自動でUndo対象になります。

細かい .registerUndo を書かなくても、取り消し・やり直しが効きます。

.onApper部分でModelContextundoManager を割り当てています。

使いどころ:フォームや一覧の“ちょい編集”を広くUndo対応にしたいときに便利です。

2. まとめて1回で戻したい(グループ化)

関連する複数の変更を1回のUndoにできます。

使いどころ:プロフィール編集の複数項目など、“ワンアクションで戻せると快適”な場面。

3. Viewの@Stateを直接戻したい(小さなプロキシ)

Viewは struct(値型)なのでターゲットにできません。Bindingを受け取る class を小さく用意してターゲットにします。

使いどころ:軽量サンプルや移行中コードで、素早くUndo対応にしたいときのテクニックです。

活用シーン

.registerUndo は「戻し方を自分で決めたい」ときに真価を発揮します。

次のケースで使うと効果がわかりやすいです。

  • カスタム編集ロジック:複雑な演算や副作用を伴う更新を、意図どおりに巻き戻したい。
  • 段階的なUI編集:色変更→サイズ変更→位置調整…といった連続操作を、まとまりの良い単位でUndoしたい。
  • SwiftData以外の状態:オンメモリのViewModelや一時的な状態管理も、.registerUndo で確実に戻せます。

注意点

以下のポイントを押さえると、安定したUndo/Redoが実装できます。

逆操作の完全性

必ず元に戻せる処理を登録します。Undoハンドラ内で元の操作を再登録し、Redoまで成立させます。

ターゲットはclass型

withTarget: には classインスタンス を渡します(weak参照のため)。View() などの値型は不可です。

参照サイクル

クロージャで self を強参照するとリークします。[weak self] を使うか、処理は ViewModel/プロキシ に寄せます。

グループ化の境界

beginUndoGrouping()endUndoGrouping()対応を必ず取ること。defer で閉じ忘れを防ぎます。

履歴サイズ

細かく記録しすぎるとメモリ・速度に影響します。必要なら levelsOfUndo で上限を設定します。

環境依存

undoManagernil のことがあります(プレビュー等)。guard let undo = undoManager else { … } で安全に扱い、ボタンも disabled を切り替えます。

SwiftData連携の前提

ModelContext.undoManager紐づけ忘れるとUndo対象になりません。ライフサイクルに合わせて確実に割り当てます。

ジェスチャと表示

シートUIでは、見た目と挙動を一致させます。誤閉じを避けたいときは .presentationDragIndicator(.hidden).interactiveDismissDisabled(true) を併用します。

まとめ

.registerUndo は、「取り消すときに実行する逆操作」を記録するメソッドです。

まずは次の流れだけ覚えれて実際に使ってみましょう!

  • @Environment(\.undoManager) を受け取り、classのターゲットに逆操作を登録する
  • 変更が複数なら グループ化 して「元に戻す」1回でまとめて戻せる

参考リンク

おすすめの記事