
「さっきの操作を取り消したい」「やっぱり元に戻したい」。
SwiftUIアプリでこの体験を実現する中核が UndoManager
です。
SwiftUIでは環境値 @Environment(\.undoManager)
から取得でき、さらに SwiftData と組み合わせると、データ編集の取り消し/やり直しも自動連携させやすくなります。
システム標準の三本指スワイプやシェイク操作とも親和性が高く、自然なUndo/Redo体験を提供できます。
UndoManagerとは?
UndoManager
は Foundationが提供するクラス(オブジェクト) で、ユーザー操作の“逆処理”を積み上げておき、後から「取り消す(Undo)」「やり直す(Redo)」を実行できるようにする仕組みです。
SwiftUI では @Environment(\.undoManager)
から参照でき、registerUndo
で逆処理を登録、undo()
/ redo()
で実行します。
SwiftData を使っている場合は、データ変更をUndoスタックに自動登録する仕組みが用意されており、最小のコードで取り消し体験を組み込めます。
実装時に覚えておきたい
UndoManager
の実装時にまず覚えておきたいプロパティ/メソッドを用途別にまとめます。
逆操作の登録(最重要)
-
registerUndo(withTarget:handler:)
取り消し時に実行する処理を登録します。ターゲットは classのインスタンス である必要があります。Undo内で「元の操作」を再登録すると Redo が成立します。 -
setActionName(_:)
「元に戻す ◯◯」の 表示名 を設定します(メニューやアクセシビリティ文言に反映されます)。
実行(Undo/Redo)
-
undo()
/redo()
直近の 取り消し/やり直し を実行します。 -
undoNestedGroup()
ネストしたグループごとに 一段だけ 取り消します(高度なケース向け)。
グループ化(複数の変更を1回で戻す)
-
beginUndoGrouping()
/endUndoGrouping()
登録した複数の操作を ひとかたまり(グループ) にします。defer { endUndoGrouping() }
で 閉じ忘れ防止 を。 -
groupingLevel
(読み取り)
現在の 入れ子レベル を確認します。
状態の問い合わせ
-
canUndo
/canRedo
(Bool)
いま 取り消せる/やり直せる かを返します(ボタンのdisabled
制御などに)。 -
isUndoing
/isRedoing
(Bool)
現在 Undo/Redoを実行中か を示します(再入防止に便利)。 -
isUndoRegistrationEnabled
(Bool)
登録を一時停止/再開 しているかを示します(大量更新の間だけ止めたいときに)。
履歴サイズとクリア
-
levelsOfUndo
(Int)
履歴の最大段数 を設定します。メモリや処理時間に合わせて上限を決めます。 -
removeAllActions()
/removeAllActions(withTarget:)
すべて(または特定ターゲット)の Undo履歴を破棄 します。
表示名の取得(必要に応じて)
-
undoActionName
/redoActionName
(String)
設定済みの アクション名 を返します。 -
undoMenuItemTitle
/redoMenuItemTitle
(String)
メニューなどに使える 完全な文言(例:「元に戻す 色の変更」)を返します。
使い方
次にUndoManager
の使い方を説明します。
基本は次の流れです。
- ユーザー操作を実行する前に、逆操作を
registerUndo
で登録します。 - 取り消し時に呼ばれる逆処理の中で、元の操作を再登録すると、Redoまで一貫して成立します。
- SwiftUIなら
@Environment(\.undoManager)
を使い、必要に応じて SwiftDataのModelContext.undoManager
に割り当てます。(Apple Developer)
補足:SwiftDataの
modelContainer
は通常、環境のundoManager
を利用します。多くの場合は追加設定なしで、システム標準のUndoジェスチャ(三本指スワイプ、シェイク)が効きます。
具体例
1. SwiftUIの状態変更にUndoを付与(ViewModel方式)
SwiftUIの View
は struct
(値型)なので、画面の更新のたびに使い捨てで作り直される前提です。
一方、Undo の呼び出し先はずっと同じ相手(同じインスタンス)である必要があります。
そこで、状態と処理を class
(参照型)の ViewModel にまとめ、その同じインスタンスを registerUndo
のターゲットにします。こうすると、Undo/Redoがいつでも同じ相手に確実に届くようになります。
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 undoManager: UndoManager? func setCount(_ newValue: Int) { let old = count undoManager?.registerUndo(withTarget: self) { me in me.setCount(old) // ← Undo中に再登録することでRedoも成立 } undoManager?.setActionName("カウントの変更") count = newValue } func increment() { setCount(count + 1) } func decrement() { setCount(count - 1) } } struct CounterView: View { @Environment(\.undoManager) private var undoManager @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("取り消す") { undoManager?.undo() } Button("やり直す") { undoManager?.redo() } } } .onAppear { model.undoManager = undoManager } .padding() } } |
ポイント
View
(値型)は再生成されやすく、Undoの“呼び戻し先”としては不安定。ViewModel
(参照型)は同一インスタンスを保てるので、registerUndo(withTarget:)
のターゲットに最適。- 実装は「
@Environment(\.undoManager)
を受け取る → ViewModelに注入 → ViewModel内でregisterUndo
」の流れにするとスッキリします。
2. SwiftData:編集の取り消しを自動連携
SwiftDataは、データ変更をUndoに自動記録できます。
明示的に ModelContext.undoManager
に環境の UndoManager
を割り当てれば、ボタンやジェスチャで直前の変更を取り消せます。
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 41 42 43 44 45 46 47 |
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 undoManager @Query private var items: [TaskItem] var body: some View { List { ForEach(items) { item in HStack { Text(item.title) Spacer() Toggle("完了", isOn: Binding( get: { item.done }, set: { newValue in item.done = newValue } )) .labelsHidden() } } } .toolbar { ToolbarItemGroup(placement: .bottomBar) { Button("追加") { context.insert(TaskItem(title: "新規タスク")) } Spacer() Button("取り消す") { undoManager?.undo() } Button("やり直す") { undoManager?.redo() } } } .onAppear { // SwiftDataのコンテキストにUndoManagerを紐づけ context.undoManager = undoManager } } } |
ポイント
ModelContext
にundoManager
をセットすると、挿入・更新・削除がUndo対象になります。modelContainer
を使う通常構成では、システム標準ジェスチャ(三本指スワイプ/シェイク)でUndo/Redoできます。
3. 複数の変更を1回のUndoにまとめる(グループ化)
関連する変更をひとかたまりにすると、ユーザーが戻しやすくなります。
1 2 3 4 5 6 7 8 9 10 11 |
func applyBatchEdits(undoManager: UndoManager, model: CounterModel) { undoManager.beginUndoGrouping() defer { undoManager.endUndoGrouping() undoManager.setActionName("バッチ編集") } model.increment() model.increment() model.decrement() } |
ポイント
-
beginUndoGrouping()
は「まとめ箱を開ける」、endUndoGrouping()
は「箱を閉じる」です。
処理の途中で早期 return しても必ず閉じるように、defer { endUndoGrouping() }
と書いておくと安全です。 -
ひとつの箱(グループ)に入れた変更は、ユーザーからは**「元に戻す」を1回実行するだけで全部まとめて**巻き戻せます。
例:名前変更+アイコン変更+自己紹介変更を同じグループに入れておけば、1回のUndoで3つとも元に戻ります。
注意点
UndoManager
は便利な一方で、実装の落とし穴もあります。
下記ポイントを押さえた上で活用しましょう。
逆操作の完全性
取り消すための逆操作を必ず登録します。
さらに、Undo処理の中で元の操作を再登録しておくと、Redoまで自然に成立します。
例:undo.registerUndo(withTarget:)
のクロージャ内で、元のメソッドをもう一度呼び出します。
参照サイクル
registerUndo
のクロージャで self
を強参照すると解放されにくくなります。
[weak self]
を使うか、処理を class
ベースの ViewModel/Proxy にまとめてターゲットにします。
グループ化の境界
beginUndoGrouping()
で“箱を開け”、最後に endUndoGrouping()
で“箱を閉じる”対応を必ず取ります。
早期リターンでも閉じ忘れないよう、defer { endUndoGrouping() }
で管理すると安全です。
履歴サイズ
細かく記録しすぎるとメモリや処理時間に影響します。
操作の粒度を見直し、必要なら levelsOfUndo
で上限を設定します。
環境依存
undoManager
は状況によって nil の場合があります。
guard let undo = undoManager else { … }
のようにオプショナルとして安全に扱い、関連ボタンも無効化するなどUI側の配慮を行います。
SwiftData連携の前提
データ編集をUndo対象にするには、ModelContext.undoManager
を明示的に紐づけます。
表示タイミングなどで一度だけ context.undoManager = undoManager
を設定しておきます。
まとめ
UndoManager
は、ユーザーが安心して試せる余地をアプリに与える重要な基盤です。
SwiftUIでは @Environment(\.undoManager)
を通じて簡単に扱え、SwiftDataと組み合わせればデータ編集のUndo/Redoも自動連携しやすくなります。
逆操作の登録→Undo中の再登録でRedo成立→必要に応じてグループ化、という基本を押さえ、自然で信頼できる編集体験を設計しましょう。
参考リンク
- EnvironmentValues.undoManager | Apple Developer Documentation
- UndoManager | Apple Developer Documentation
- ModelContext.undoManager | Apple Developer Documentation
- Reverting data changes using the undo manager | Apple Developer Documentation