Last active
September 26, 2022 15:59
-
-
Save monyschuk/e2c5609599195a30cc66 to your computer and use it in GitHub Desktop.
Simple Swift FSM
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: Playground - noun: a place where people can play | |
import UIKit | |
/// A finite state machine | |
final class FSM<State, Transition>: ObservableObject where State: Hashable, Transition: Hashable { | |
/// current state | |
@Published private(set) var state: State | |
/// state transition graph | |
private(set) var transitions = [State:[Transition:State]]() | |
/// Adds a transition `transition` from state `first` to state `second` | |
/// - Parameters: | |
/// - transition: a transition | |
/// - first: the starting state | |
/// - second: the ending state | |
func addTransition(_ transition: Transition, from first: State, to second: State) { | |
transitions[first, default: [:]][transition] = second | |
} | |
/// Returns `true` if `transition` will advance state | |
/// - Parameter transition: a transition | |
/// - Returns: `true` if the transition will advance state | |
func canAdvance(_ transition: Transition) -> Bool { | |
return transitions[state]?[transition] != nil | |
} | |
/// a transition observation function | |
typealias Observer = (_ old: State, _ new: State) -> () | |
/// Advances state by performing `transition`. | |
/// - Parameters: | |
/// - transition: a transition | |
/// - observe: an optional transition observer | |
/// - Returns: the new state | |
@discardableResult | |
func transition(_ transition: Transition, observe: Observer? = nil) -> State { | |
let prev = state | |
if let next = transitions[prev]?[transition], next != prev { | |
state = next | |
observe?(prev, next) | |
} | |
return state | |
} | |
/// a transition builder function, used to add transitions to a newly created state machine | |
typealias TransitionBuilder = (FSM<State,Transition>)->() | |
/// Initializes the state machine with state `state` then optionally executes `builder` where | |
/// transitions between states can be configured on the receiver. If `builder` is nil, then transitions | |
/// can be added to the state machine after initialization | |
/// - Parameters: | |
/// - state: a starting state | |
/// - builder: a transition builder | |
init(state: State, transitions builder: TransitionBuilder? = nil) { | |
self.state = state | |
builder?(self) | |
} | |
fileprivate init(state: State, transitions: [State:[Transition:State]]) { | |
self.state = state | |
self.transitions = transitions | |
} | |
} | |
extension FSM: Equatable { | |
static func ==(lhs: FSM<State,Transition>, rhs: FSM<State,Transition>) -> Bool { | |
return lhs === rhs || lhs.state == rhs.state && lhs.transitions == rhs.transitions | |
} | |
} | |
extension FSM: Codable where State: Codable, Transition: Codable { | |
enum CodingKeys: String, CodingKey { | |
case state | |
case transitions | |
} | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.container(keyedBy: CodingKeys.self) | |
try container.encode(state, forKey: .state) | |
try container.encode(transitions, forKey: .transitions) | |
} | |
convenience init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
let state = try container.decode(State.self, forKey: .state) | |
let transitions = try container.decode([State:[Transition:State]].self, forKey: .transitions) | |
self.init(state: state, transitions: transitions) | |
} | |
} | |
// | |
// Sample Usage | |
// | |
enum Switch { | |
case on, off | |
} | |
enum Position { | |
case up, down | |
} | |
var lights = FSM<Switch, Position>(state: .off) { | |
fsm in | |
fsm.addTransition(.up, from: .off, to: .on) | |
fsm.addTransition(.down, from: .on, to: .off) | |
} | |
// you can observe state changes, since FSM is an `ObservableObject` | |
let obs = lights.$state.sink(receiveValue: { | |
print($0) | |
}) | |
// sending transitions to the FSM causes its state to change for defined state changes | |
lights.transition(.up) | |
lights.transition(.down) | |
lights.transition(.down) | |
lights.transition(.up) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@ordovician
It's been a while, but Swift now makes this easy to do without bloating the code, so I've updated the gist to make
StateMachine
(nowFSM
) anObservableObject
with an@Published
state.