Swift
statemachine
RxSwift

Swiftで簡単なステートマシンを作って使う

フラグで状態を管理するのに限界を感じたので、簡単なステートマシンを作ってみました。

簡単なしくみの割に、使い勝手がよくてお気に入りです。

StateMachine.swift
class StateMachine<State, Event> {
    init(_ initial: State, routing:@escaping (State, Event) -> State?) {
        self.current = initial
        self.routing = routing
    }

    private(set) var current: State
    var delegate: StateMachineDelegate?

    private let routing: (State, Event) -> State?

    func transition(_ by: Event) {
        guard let next = routing(current, by) else {
            return
        }

        delegate?.stateMachine(self, notified: .willExit(current))
        current = next
        delegate?.stateMachine(self, notified: .didEnter(next))
    }
}

protocol StateMachineDelegate {
    func stateMachine<State, Event>(_ stateMachine: StateMachine<State, Event>, notified change: StateChange<State>)
}

enum StateChange<T> {
    case didEnter(T)
    case willExit(T)
}

本体はこれだけです。

使い方

  1. 初期状態と状態遷移関数を指定して、StateMachineクラスをインスタンス化する
  2. transitionメソッドで状態を遷移させる
  3. 状態が遷移すると遷移イベントが通知されるので、必要な処理が実行されるようにdelegateを実装しておく

例えば入力画面で以下のような状態遷移をしたい場合、

sample.png

[*] -right-> 入力中
入力中 --> 確認中 : 保存
確認中 -right-> 保存中 : 確認OK
確認中 --> 入力中 : 確認NG
保存中 -right-> 保存後 : 保存終了
保存後 --> 入力中 : 結果表示

まず必要な状態とイベントをenumで定義します。

enum EditState {
    /// 入力中
    case editing
    /// 確認中
    case confirming
    /// 保存中
    case saving
    /// 保存後
    case saved(SaveResult)
}

enum EditEvent {
    /// 保存
    case save
    /// 確認(OK/NG)
    case confirm(Bool)
    /// 保存終了
    case finishSave(SaveResult)
    /// 結果表示
    case shownResult
}

enum SaveResult {
    case success
    case fail(Error)
}

使うときは以下のような感じです。

最初にステートマシンを作って、遷移イベントを起こす時は transition(_ :Event) を呼びます。

状態が遷移するとデリゲートで通知されるので、そのタイミングで必要な処理をします。この例だと、ダイアログを出したり、保存処理を開始したりします。

class EditViewController: StateMachineDelegate {
    init() {
        // 状態遷移を定義
        self.state = StateMachine<EditState, EditEvent>(.editing) { (state, event) in
            switch (state, event) {
            // 入力中 --> 確認中 : 保存
            case (.editing, .save):
                return .confirming
            // 確認中 --> 保存中 : 確認OK
            case (.confirming, .confirm(true)):
                return .saving
            // 確認中 --> 入力中 : 確認NG
            case (.confirming, .confirm(false)):
                return .editing
            // 保存中 --> 保存後 : 保存終了
            case (.saving, .finishSave(let result)):
                return .saved(result)
            // 保存終了 --> 入力中 : 結果表示
            case (.saved, .shownResult):
                return .editing
            default:
                return nil
            }
        }
        state.delegate = self
    }

    var state: StateMachine<EditState, EditEvent>

    // 保存ボタンのタップイベント
    func tapSaveButton() {
        state.transition(.save)
    }

    //MARK:-状態変化のアクション
    func stateMachine<State, Event>(_ stateMachine: StateMachine<State, Event>, notified change: StateChange<State>) {
        let c = change as! StateChange<EditState>
        switch c {
        case .didEnter(.confirming):
            self.didEnterConfirming()
        case .didEnter(.saving):
            self.didEnterSaving()
        case .didEnter(.saved(let result)):
            self.didEnterSaved(result: result)
        default:
            break
        }
    }

    private func didEnterConfirming() {
        // 確認ダイアログを表示する
        // ...
        // ... OK/NGが選択されたら state.transition(.confirm(Bool)) を呼ぶ
    }

    private func didEnterSaving() {
        // 保存処理を開始する
        // ...
        // ... 保存処理が終わったら state.transition(.finishSave(SaveResult)) を呼ぶ
    }

    private func didEnterSaved(result: SaveResult) {
        switch result {
        case .success:
            // 保存に成功したことを表示する
            // ...
            break
        case .fail(let error):
            // 保存に失敗したことを表示する
            // ...
            break
        }

        state.transition(.shownResult)
    }
}

RxSwift版

RxSwiftを使えばデリゲートが要りません。
実際にはこちらを使ってます。

StateMachine.swift
import RxSwift

class StateMachine<State, Event> {
    init(_ initial: State, routing:@escaping (State, Event) -> State?) {
        self.current = initial
        self.routing = routing
        changedSubject.disposed(by: disposeBag)
    }

    private(set) var current: State
    lazy var changed = self.changedSubject.asObservable()

    private let routing: (State, Event) -> State?
    private let changedSubject = PublishSubject<StateChange<State>>()
    private let disposeBag = DisposeBag()

    func transition(_ by: Event) {
        guard let next = routing(current, by) else {
            return
        }

        changedSubject.onNext(.willExit(current))
        current = next
        changedSubject.onNext(.didEnter(next))
    }
}

enum StateChange<T> {
    case didEnter(T)
    case willExit(T)
}

参考元

参考にさせて頂きました。