「さっきの変更を取り消したい」「やっぱり元に戻したい」。その“戻すための処理”をあらかじめ登録しておくのが .registerUndo です。
SwiftUI では @Environment(\.undoManager) から取得した UndoManager に対して呼び出し、取り消すときに実行される逆操作を記録します。
登録のコツさえつかめば、Redo(やり直し)まで自然に動かせます。
.registerUndoとは?
.registerUndo(withTarget:_:) は UndoManagerのメソッドで、取り消し時に呼ぶ処理を登録します。
“箱に逆再生テープを入れておく”イメージです。
ユーザーが「元に戻す」を実行すると、そのテープが再生されて状態が巻き戻ります。
- ターゲットは class(参照型)必須:
UndoManagerはターゲットを weak参照 するため、struct(例:SwiftUIのView)は渡せません。classな ViewModel や小さなプロキシをターゲットにします。 - Redoを効かせるコツ:Undo処理の中で“元の操作”をもう一度登録します。Undo ↔ Redo を往復できるようになります。
使い方
基本手順は次のとおりです。
- UndoManagerを用意(SwiftUIなら
@Environment(\.undoManager))。 - 安定したターゲット(class)を決める(ViewModel など)。
- 変更前の値を保持し、
.registerUndo(withTarget:)の中で戻す処理を書く。 - 実際の変更を行い、必要なら
.setActionName(_:)で表示名を付ける。 - Undoハンドラの中で元の操作を再登録して、Redoまで成立させる。
最小コード(ViewModel方式)です。
.registerUndoのwithTarget: には、取り消し時に“呼び戻したい相手”=上のコードでは withTarget: self を渡しています。
つまり CounterModel の同一インスタンスが「呼び戻したい相手(ターゲット)」です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import SwiftUI final class CounterModel: ObservableObject { @Published var count = 0 var undo: UndoManager? func setCount(_ newValue: Int) { let old = count undo?.registerUndo(withTarget: self) { me in me.setCount(old) // ← Undo時に再登録 → Redoも自然に成立 } undo?.setActionName("カウントの変更") count = newValue } func increment() { setCount(count + 1) } func decrement() { setCount(count - 1) } } struct CounterView: View { @Environment(\.undoManager) private var undo @StateObject private var model = CounterModel() var body: some View { VStack(spacing: 12) { Text("Count: \(model.count)") HStack { Button("−1") { model.decrement() } Button("+1") { model.increment() } } HStack { Button("取り消す") { undo?.undo() } Button("やり直す") { undo?.redo() } } } .onAppear { model.undo = undo } // UndoManagerを注入 .padding() } } |
上記コードの undo?.registerUndo(withTarget: self) { me in部分がだいぶわかりにくいと思うので下記解説します。
me.setCount(old) // ← Undo時に再登録 → Redoも自然に成立
}
何が起きているか(時系列)
setCount(_:)を呼ぶとき、まず 「元の値へ戻す処理」 をregisterUndoで登録します。
具体的には「あとで Undo されたらsetCount(old)を呼んでね」と箱に入れておきます。- そのあとで実際に
count = newValueに更新します。 - ユーザーが Undo を押すと、箱の中身が実行されて
setCount(old)が呼ばれます。 - ここがポイント:
setCountは「呼ばれるたびに逆操作を登録する」メソッドなので、Undo 中に呼ばれたsetCount(old)も “逆方向” を登録します。
つまり「次に Redo されたらsetCount(newValue)を呼んでね」が箱に入る。これが Redo 用の登録 です。 - だから 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 対象にする
ModelContext に undoManager を割り当てると、挿入・更新・削除が自動でUndo対象になります。
細かい .registerUndo を書かなくても、取り消し・やり直しが効きます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import SwiftUI import SwiftData @Model final class TaskItem { var title: String var done: Bool init(title: String, done: Bool = false) { self.title = title; self.done = done } } struct TaskListView: View { @Environment(\.modelContext) private var context @Environment(\.undoManager) private var undo @Query private var items: [TaskItem] var body: some View { List { ForEach(items) { item in Toggle(item.title, isOn: Binding( get: { item.done }, set: { item.done = $0 } // ← これだけでUndo対象になる )) } } .toolbar { Button("取り消す") { undo?.undo() } Button("やり直す") { undo?.redo() } } .onAppear { context.undoManager = undo } // ← 忘れるとUndoされません } } |
.onApper部分でModelContext に undoManager を割り当てています。
使いどころ:フォームや一覧の“ちょい編集”を広くUndo対応にしたいときに便利です。
2. まとめて1回で戻したい(グループ化)
関連する複数の変更を1回のUndoにできます。
|
1 2 3 4 5 6 7 8 9 10 |
func applyBatchEdits(undo: UndoManager, model: CounterModel) { undo.beginUndoGrouping() defer { undo.endUndoGrouping(); undo.setActionName("バッチ編集") } model.increment() model.increment() model.decrement() } // ユーザーからは「元に戻す」1回で、上の3つがまとめて戻る |
使いどころ:プロフィール編集の複数項目など、“ワンアクションで戻せると快適”な場面。
3. Viewの@Stateを直接戻したい(小さなプロキシ)
Viewは struct(値型)なのでターゲットにできません。Bindingを受け取る class を小さく用意してターゲットにします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
final class IntBindingProxy: NSObject { private var binding: Binding<Int> init(_ binding: Binding<Int>) { self.binding = binding } func set(_ value: Int) { binding.wrappedValue = value } } struct TinyView: View { @Environment(\.undoManager) private var undo @State private var count = 0 @StateObject private var holder = Holder() final class Holder: ObservableObject { var proxy: IntBindingProxy? } var body: some View { Button("+1") { let old = count guard let undo else { return } if holder.proxy == nil { holder.proxy = IntBindingProxy($count) } // 強参照で保持 undo.registerUndo(withTarget: holder.proxy!) { p in p.set(old) } count += 1 } } } |
使いどころ:軽量サンプルや移行中コードで、素早くUndo対応にしたいときのテクニックです。
活用シーン
.registerUndo は「戻し方を自分で決めたい」ときに真価を発揮します。
次のケースで使うと効果がわかりやすいです。
- カスタム編集ロジック:複雑な演算や副作用を伴う更新を、意図どおりに巻き戻したい。
- 段階的なUI編集:色変更→サイズ変更→位置調整…といった連続操作を、まとまりの良い単位でUndoしたい。
- SwiftData以外の状態:オンメモリのViewModelや一時的な状態管理も、
.registerUndoで確実に戻せます。
注意点
以下のポイントを押さえると、安定したUndo/Redoが実装できます。
逆操作の完全性
必ず元に戻せる処理を登録します。Undoハンドラ内で元の操作を再登録し、Redoまで成立させます。
ターゲットはclass型
withTarget: には classインスタンス を渡します(weak参照のため)。View や () などの値型は不可です。
参照サイクル
クロージャで self を強参照するとリークします。[weak self] を使うか、処理は ViewModel/プロキシ に寄せます。
グループ化の境界
beginUndoGrouping() と endUndoGrouping() の対応を必ず取ること。defer で閉じ忘れを防ぎます。
履歴サイズ
細かく記録しすぎるとメモリ・速度に影響します。必要なら levelsOfUndo で上限を設定します。
環境依存
undoManager が nil のことがあります(プレビュー等)。guard let undo = undoManager else { … } で安全に扱い、ボタンも disabled を切り替えます。
SwiftData連携の前提
ModelContext.undoManager を紐づけ忘れるとUndo対象になりません。ライフサイクルに合わせて確実に割り当てます。
ジェスチャと表示
シートUIでは、見た目と挙動を一致させます。誤閉じを避けたいときは .presentationDragIndicator(.hidden) と .interactiveDismissDisabled(true) を併用します。
まとめ
.registerUndo は、「取り消すときに実行する逆操作」を記録するメソッドです。
まずは次の流れだけ覚えれて実際に使ってみましょう!
@Environment(\.undoManager)を受け取り、classのターゲットに逆操作を登録する- 変更が複数なら グループ化 して「元に戻す」1回でまとめて戻せる
参考リンク
- UndoManager.registerUndo(withTarget:handler:) | Apple Developer Documentation
- UndoManager | Apple Developer Documentation
- EnvironmentValues.undoManager | Apple Developer Documentation
- Reverting data changes using the undo manager (SwiftData) | Apple Developer Documentation