Skip to content

Instantly share code, notes, and snippets.

@jemmons
Created February 10, 2015 04:07
Show Gist options
  • Star 44 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jemmons/f30f1de292751da0f1b7 to your computer and use it in GitHub Desktop.
Save jemmons/f30f1de292751da0f1b7 to your computer and use it in GitHub Desktop.
Updated State Machine with Untangled Transitions and Behaviors
import Foundation
protocol StateMachineDelegate: class{
typealias StateType:StateMachineDataSource
func didTransitionFrom(from:StateType, to:StateType)
}
protocol StateMachineDataSource{
func shouldTransitionFrom(from:Self, to:Self)->Should<Self>
}
enum Should<T>{
case Continue, Abort, Redirect(T)
}
class StateMachine<P:StateMachineDelegate>{
private var _state:P.StateType{
didSet{
delegate.didTransitionFrom(oldValue, to: _state)
}
}
unowned let delegate:P
var state:P.StateType{
get{
return _state
}
set{ //Can't be an observer because we need the option to CONDITIONALLY set state
switch _state.shouldTransitionFrom(_state, to:newValue){
case .Continue:
_state = newValue
case .Redirect(let redirectState):
_state = newValue
self.state = redirectState
case .Abort:
break;
}
}
}
init(initialState:P.StateType, delegate:P){
_state = initialState //set the primitive to avoid calling the delegate.
self.delegate = delegate
}
}
enum NetworkState:StateMachineDataSource{
case Ready, Success(NSDictionary), Fail(NSError)
func shouldTransitionFrom(from:NetworkState, to: NetworkState) -> Should<NetworkState>{
switch (from, to){
case (.Ready, .Success), (.Ready, .Fail):
return .Redirect(.Ready)
case (.Success, .Ready), (.Fail, .Ready):
return .Continue
default:
return .Abort
}
}
}
class MyClass:StateMachineDelegate{
let machine:StateMachine<MyClass>!
init(){
machine = StateMachine(initialState: .Ready, delegate: self)
}
typealias StateType = NetworkState
func didTransitionFrom(from: StateType, to: StateType) {
switch (from, to){
case (.Ready, .Success(let json)):
updateModel(json)
case (.Ready, .Fail(let error)):
handleError(error)
case (_, .Ready):
reloadInterface()
default:
break
}
}
}
@MartinJNash
Copy link

I was able to completely get rid of the delegate's shouldTransitionFromState:ToState: method by instantiating the state machine with a list of valid transitions. This will prevent the urge to put side effects in the 'should' method.

import Foundation

class StateMachine<P:StateMachineDelegateProtocol>{

    private unowned let delegate:P
    private let validTransitions: [P.StateType: [P.StateType]]

    private var _state:P.StateType{
        didSet{
            delegate.didTransitionFrom(oldValue, to:_state)
        }
    }

    var state:P.StateType{
        get{
            return _state
        }
        set{ //Can't be an observer because we need the option to CONDITIONALLY set state
            attemptTransitionTo(newValue)
        }
    }


    init(initialState:P.StateType, delegate:P, validTransitions: [P.StateType: [P.StateType]]){
        _state = initialState //set the primitive to avoid calling the delegate.
        self.validTransitions = validTransitions
        self.delegate = delegate
    }


    private func attemptTransitionTo(to:P.StateType){
        if let validNexts = validTransitions[_state] {
            if contains(validNexts, to) {
                _state = to
            } else {
                // error, etc
            }
        }
    }
}

protocol StateMachineDelegateProtocol: class {
    // make this be hashable so we can pass in a dictionary
    typealias StateType : Hashable

    func didTransitionFrom(from:StateType, to:StateType)
}





import UIKit

class Example : UIView{

    private var machine:StateMachine<Example>!

    enum TrafficLight : Int {
        case Stop, Go, Caution
    }


    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        let tx = [
            TrafficLight.Stop: [TrafficLight.Go],
            TrafficLight.Caution: [TrafficLight.Stop],
            TrafficLight.Go: [TrafficLight.Caution]
        ]

        machine = StateMachine(initialState: .Stop, delegate: self, validTransitions: tx)
    }


    @IBAction func tappedGo(sender:AnyObject){
        machine.state = .Go
    }


    @IBAction func tappedCaution(sender:AnyObject){
        machine.state = .Caution
    }
}

extension Example : StateMachineDelegateProtocol {

    typealias StateType = TrafficLight

    func didTransitionFrom(from: StateType, to: StateType) {
        switch to{
        case .Stop:
            backgroundColor = UIColor.redColor()
        case .Go:
            backgroundColor = UIColor.greenColor()
        case .Caution:
            backgroundColor = UIColor.yellowColor()
        }
    }

}

@happyjem
Copy link

these seems to compile error in Xcode 6.3 beta 2 (I use Swift 1.2)

In MyClass

'self' used before all stored properties are initialized
'self.machine' used before being initialized

@happyjem
Copy link

let machine:StateMachine! ====> var machine:StateMachine!
Then compiled to success

@happyjem
Copy link

I moved to "unowned let delegate:P" above "private var _state:P.StateType" then got compiler error that "Command /Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc failed with exit code 1"

I don't Know what´s wrong with this function

@iliaskarim
Copy link

I think @MartinJNash is on the right path by removing the should delegate method and transforming its representation into data.

I also noticed @algal's comments on the previous iteration of the state machine. He thought that the delegate pattern was a mismatch for the generics in use. I tend to agree.

I finally ended up with this (Traffic Light example in link, only the State and State Machine are reproduced below for brevity)

protocol State {

    func shouldTransition(toState: Self) -> Bool
}


class StateMachine<T: State>{

    typealias TransitionObservation = ((from: T, to: T) -> ())

    var state: T {
        get { return _state }
        set {
            if (self.state.shouldTransition(newValue)) {
                let oldValue = _state
                _state = newValue
                self.transitionObservation?(from: oldValue, to: newValue)
            }
        }
    }

    private var _state: T

    private var transitionObservation: TransitionObservation?

    init(initialState:T, observer: TransitionObservation? = nil) {
        _state = initialState
        self.transitionObservation = observer
    }
}

It has a clear delineation between State, State Machine, and Observer, meaning:

  1. The State can explicitly declare which transitions are valid
  2. There is no possibility of shoehorning State change side effects into the State Machine itself

I've also added the feature to add as many observers as one likes (see my gist), which can be a very useful thing in a world without KVO!

Let me know what you think

@Codeglee
Copy link

Codeglee commented Jan 3, 2017

@ILI4S Your gist is a 404, I'd be interested in seeing what you came up with though.

@iOSSmith
Copy link

This is gold!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment