LoginSignup
31
28

More than 5 years have passed since last update.

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

Posted at

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

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

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)
}

参考元

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

31
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
28