Figure

A blog about Swift and iOS development.

Swift State Machines, Part 2

This is a two-parter. Part 1 covered some quick background about why our objects are broken, why that's not our fault, and how we can fix them using state machines.

Alright. Now let's build us one!

The Swift State Machine

The biggest impediment to using existing state machine libraries is getting them configured. Before we can get off the ground in some of these packages, we have to define not only all our states, but also all of their transitions. Often this requires a lot of ugly factories, redundant boilerplate, or even XML.

In our Swift state machine, we're going to get rid of this cruft the Cocoa way: delegation! And as we'll see, Swift has some nice features that help us keep our delegates slim as well.

But let's start with the machine itself. All we really need is a property to hold the current state and an initializer:

class StateMachine{
  //We'll define StateType in a moment...
  var state:StateType   
  init(initialState:StateType){
    state = initialState
  }
}

Easy, right? Now let's add our delegate. We'll start with its protocol. We want it to do three things:

  • Tell us what the valid states are,
  • Decide whether a given transition should be allowed to happen, and
  • Optionally do something after the transition.

We'll do all this with an associated type, a method that returns a Bool, and a Void method that gives the delegate a chance to do some processing.

protocol StateMachineDelegateProtocol:class{
  typealias StateType
  func shouldTransitionFrom(from:StateType, to:StateType)->Bool
  func didTransitionFrom(from:StateType, to:StateType)
}

[UPDATE: See Part 3 for some new ideas on whether didTransitionFrom(to:) should live in the delegate or not.]

Note that we'd probably like didTransitionFrom(to:) to be optional. That's not possible in pure Swift protocols yet, but it's planned for a future release.

Also check out that we've made this a class-only protocol. This is de rigueur for delegate protocols. After all, on a philosophical level, a delegate with value semantics doesn't really make any sense. More pragmatically, delegate properties should almost always be weak or unowned. And values, of course, can't be either.

So let's add a delegate to our machine. This gets a little hairy, genericly-speaking, because we have to use the delegate protocol as a generic parameter (see the previous re: Generic Delegate Protocols for details):

class StateMachine<P:StateMachineDelegateProtocol>{
  private unowned let delegate:P
  var state:P.StateType

  init(initialState:P.StateType, delegate:P){
    state = initialState
    self.delegate = delegate
  }
}

Now we have to wire up calls to our delegate to both confirm we want to switch state, and to let it do something after we have successfully switched state.

Swift doesn't provide us a way to conditionally set a stored property in its setter1. So we're going to make a public computed property that sits in front of a private stored property and conditionally sets it depending on the return from our delegate:

private var _state:P.StateType

var state:P.StateType{
  get{ return _state }
  set{
    if delegate.shouldTransitionFrom(_state, to:newValue){
      _state = newValue
    }
  }
}

Then we'll add an observer to our stored property that calls our delegate whenever it's been set:

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

Finally, becasue setting the initial state of our machine isn't tecnically a "transition", we don't want to have to call shouldTransitionFrom(to) when we do it. In our initializer, then, we'll assign initialState directly to the stored property instead of our computed one. And we're all set!

class StateMachine<P:StateMachineDelegateProtocol>{
  private unowned let delegate:P
  private var _state:P.StateType{
    didSet{ delegate.didTransitionFrom(oldValue, to:_state) }
  }
  var state:P.StateType{
    get{ return _state }
    set{
      if delegate.shouldTransitionFrom(_state, to:newValue){
        _state = newValue
      }
    }
  }

  init(initialState:P.StateType, delegate:P){
    _state = initialState
    self.delegate = delegate
  }
}

Transitional Awareness

That's a nice looking machine… so how do we use it?

Because our delegate methods are so general, there's no one right answer. But as I teased earlier, Swift has some nice tools for implementing methods like this. Namely enums, tuples, and the switch statement.

The first thing we'll need to do is define our states, and that means assigning a concrete enum to our protocol's associated type:

class MyClass:StateMachineDelegateProtocol{
  enum AsyncNetworkState{
    case Ready, Fetching, Saving
    case Success(NSDictionary)
    case Failure(NSError)
  }
  typealias StateType = AsyncNetworkState
}

Next, we'll let our machine know when a given transition is allowed by implementing shouldTransitionFrom(to:). We could use a bunch of if statements or a big nested switch to iterate over all possible pairings of our states2. But instead, we're going to wrap all our params up in a tuple, switch on that, and use default and some of Swift's cool pattern matching to make short work of it:

func shouldTransitionFrom(from:StateType, to:StateType)->Bool{
  switch (from, to){
  case (.Ready, .Fetching), (.Ready, .Saving):
    return true
  case (.Fetching, .Success), (.Fetching, .Failure):
    return true
  case (.Saving, .Success), (.Saving, .Failure):
    return true
  case (_, .Ready):
    return true
  default:
    return false
  }
}

Let's start at the bottom. By default, any transition between states we don't expressly define simply won't happen. That should already make us feel warmer and fuzzier compared to the status quo.

Moving back to the top, we specifically approve transitions from Ready to any of our async states, and then from any of our async states to either Success or Failure. There's no real reason to break these up on multiple lines — I often don't. They're separated here just for readability.

Finally we use Swift's wildcard pattern to approve any transition to our Ready state — all in a single line.

And that's it! Even this liberally formatted example squeaks by at 14 lines of very readable case statements.

Doing Stuff With State

We'll follow the same general pattern to actually get stuff done when the state changes:

func didTransitionFrom(from:StateType, to:StateType){
  switch (from, to){
  case (.Ready, .Fetching):
    task = myAPI.fetchRequestWithCompletion{ ... }
  case (.Ready, .Saving):
    task = myAPI.saveRequestWithCompletion{ ... }
  }
}

Here, we're sending off the appropriate request whenever the Fetching or Saving is transitioned to from a Ready state. Note that, in contrast to our example from Part 1, we're now relying on the simple state of our system rather than the complex value of our task property to manage our requests. If we imagine a new implementation of our fetch action that looks something like:

func fetchThing(params:NSDictionary){
    machine.state = .Fetching
}

We can see that, because FetchingFetching is an invalid transition not allowed by our delegate, it doesn't matter how many times we call fetchThing. We'll still only get a single request sent3.

The last trick up our state machine's sleeve is that we can use associated values in our enums to actually pass data around as we switch state. A fuller implementation of the above might look something like:

func didTransitionFrom(from:StateType, to:StateType){
  switch (from, to){
  case (.Ready, .Fetching):
    myAPI.fetchRequestWithCompletion{json, error in
      if let someError = error{
        machine.state = .Failure(someError)
      } else{
        machine.state .Success(json)
      }
    }
  case (_, .Failure(let error)):
    displayGeneralError(error)
    machine.state = .Ready
  case (.Fetching, .Success(let json)):
    parseFetchSpecificJSON(json)
    machine.state = .Ready
  case (_, .Ready):
    updateInterface()
}

Our request's completion handler, like most actions, can be reduced to nothing more than state transitions. And once we reduce it so, reasoning about our app's interactions4 becomes much simpler. We no longer have to ask ourselves "Which actions cause the interface to get updated?" It's a simple matter to see what state updates the UI (the Ready state in the example above) and what states are allowed to transition to it (Success and Failure).

Wrap Up

Once again, state machines don't make this stuff easy. We've still got to reason through the use cases and write the code. But what state machines will do is keep our code abstract by decoupling concepts that never should have been coupled in the first place, opening the door to whole new worlds of refactoring.

Giving ourselves a dedicated structure to store state is just the first step.

The inline code above is a little disjointed in the name of constructing a narrative, so I've made a sample gist available of all the stuff we've talked about. Pull requests welcome!

[UPDATE: There are some great comments on this gist! Be sure to check them out and also Swift State Machines, Part 3, which responds to some of them.]


1: If our property has a setter, then it is, by definition, a computed property. And computed properties have no storage.↩︎

2: Five states mapped over both the from and to params in our delegate would be 25 (52) distinct transition types. Ick!↩︎

3: That is, until our state returns to Ready, as that is the only state allowed to transition to Fetching.↩︎

4: Extra especially its asynchronous interactions.↩︎


Hit me up on twitter (@jemmons) to continue the conversation.

Latest | Archives