⏪
SwiftUI+TCAでUndo / Redo
はじめに
TCAでUndo / Redoを実装する方法を紹介します。
作るもの
Undo / Redo機能を持つカウンターアプリを作ります。
準備
Swift Package ManagerでTCAをインストールします。
コード全体
import SwiftUI
import ComposableArchitecture
struct CounterFeature: ReducerProtocol {
// #1
struct State: Equatable {
var count: Int
var undoStack: [Action] = []
var redoStack: [Action] = []
}
// #2
enum Action: Equatable {
case setCount(Int)
case incrementDecrement(IncrementDecrementAction)
case undoRedo(UndoRedoAction)
enum IncrementDecrementAction: Equatable {
case increment
case decrement
case increment2
case decrement2
}
enum UndoRedoAction: Equatable {
case undo
case redo
}
}
var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case let .setCount(count):
state.count = count
// #3
case let .incrementDecrement(action):
state.undoStack.append(.setCount(state.count))
state.redoStack = []
switch action {
case .increment:
state.count += 1
case .decrement:
state.count -= 1
case .increment2:
state.count += 2
case .decrement2:
state.count -= 2
}
case let .undoRedo(action):
switch action {
case .undo:
// #4
guard let last = state.undoStack.popLast() else { return .none }
state.redoStack.append(.setCount(state.count))
return .task { last }
case .redo:
// #5
guard let last = state.redoStack.popLast() else { return .none }
state.undoStack.append(.setCount(state.count))
return .task { last }
}
}
return .none
}
}
}
struct CounterView: View {
let store: StoreOf<CounterFeature>
var body: some View {
WithViewStore(
store,
observe: { $0 }
) { viewStore in
NavigationStack {
VStack {
Text("\(viewStore.count)")
.font(.title)
HStack(spacing: 16) {
Button("-2") {
viewStore.send(.incrementDecrement(.decrement2))
}
Button("-1") {
viewStore.send(.incrementDecrement(.decrement))
}
Button("+1") {
viewStore.send(.incrementDecrement(.increment))
}
Button("+2") {
viewStore.send(.incrementDecrement(.increment2))
}
}
.controlSize(.large)
.buttonStyle(.bordered)
}
.toolbar {
Button {
viewStore.send(.undoRedo(.undo))
} label: {
Image(systemName: "arrow.uturn.backward")
}
.disabled(viewStore.undoStack.isEmpty)
Button {
viewStore.send(.undoRedo(.redo))
} label: {
Image(systemName: "arrow.uturn.forward")
}
.disabled(viewStore.redoStack.isEmpty)
}
}
}
}
}
解説
#1
Stateを定義します。
undoStackとredoStackはActionの配列です。
#2
Actionを定義します。
undoStackは、undo時に使うActionを保存しておくためのものです。
redoStackは、redo時に使うActionを保存しておくためのものです。
#3
カウントアップ/カウントダウンするそれぞれのActionは、処理を実行する前の状態をセットするActionをundoStackに追加します。
#4
undo時にundoStackからActionを取り出して、実行します。
redoStackには、undoする前の状態をセットするActionを追加します。
#5
redo時にredoStackからActionを取り出して、実行します。
undoStackには、redoする前の状態をセットするActionを追加します。
おわりに
TCAでUndo / Redoを実装する方法を紹介しました。
まだまだ改善の余地があると思うので、フィードバックいただけると嬉しいです。
Discussion