SwiftUI+TCAでUndo / Redo

2023/07/05に公開

はじめに

TCAでUndo / Redoを実装する方法を紹介します。

作るもの

Undo / Redo機能を持つカウンターアプリを作ります。

https://twitter.com/shota_appdev/status/1671158013556846595?s=61&t=GouZrcwnH5SI9h0ROcgYWA

準備

Swift Package ManagerでTCAをインストールします。

https://github.com/pointfreeco/swift-composable-architecture

コード全体

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