フラグで状態を管理するのに限界を感じたので、簡単なステートマシンを作ってみました。
簡単なしくみの割に、使い勝手がよくてお気に入りです。
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)
}
本体はこれだけです。
使い方
- 初期状態と状態遷移関数を指定して、StateMachineクラスをインスタンス化する
- transitionメソッドで状態を遷移させる
- 状態が遷移すると遷移イベントが通知されるので、必要な処理が実行されるようにdelegateを実装しておく
例
例えば入力画面で以下のような状態遷移をしたい場合、
[*] -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)
}
参考元
参考にさせて頂きました。