Swift's enum as a data source state machine

  • Published 13 Feb 2015

Last week at dotSwift.io, two of the presentations contained a version of the following example of using associated values of Swift enumerations:

enum State<D> {
  case Empty
  case Loading
  case Ready(D)
  case Error(NSError)
}

struct DataSource<T> {
  var state: State<T>
}

This is nice1, because it can replace the separate variables for keeping state and the data associated with it:

struct DataSource<T> {
  var loading: Bool
  var data: T?
  var error: NSError?
}

This is of course a great example for a presentation, but it makes the assumption that you only want to retrieve that data in the .Ready state. But what if you want to reload the data? You will have to switch to the .Loading state, but because the data is associated with the .Ready state, you can’t access the previously loaded data anymore. And even worse: if the reloading fails and you end up in the .Error state, you can’t even show the data that was shown before we reloaded!

State machine

If we look at the above image, we see that we need a representation of data in three of the four states. We can model that with optional associated values as follows:

enum State<D> {
    case Empty
    case Loading(D?)
    case Ready(D)
    case Error(NSError,D?)
}

Ouch. That suddenly doesn’t look as nice anymore, does it? But bear with me, we can fix this. We can define a computed property on the state enum that provides us with the data or error, no matter what the current state is:

extension State {
    var data: D? {
        switch self {
        case .Empty:
            return nil
        case .Ready(let data):
            return data
        case .Loading(let data):
            return data
        case .Error(_, let data):
            return data
        }
    }

    var error: NSError? {
        switch self {
        case .Error(let error, _):
            return error
        default:
            return nil
        }
    }  
}

Ok, now we can retrieve the data in a better way, but what about associating the data with the enum? That can also be handled by defining some mutating functions on the enum:

extension State {
    func toLoading() -> DataSourceState {
        switch self {
        case .Ready(let oldData):
            let value: D? = oldData.value
            return .Loading(Box(value))
        default:
            return .Loading(Box(nil))
        }
    }

    func toError(error:E) -> DataSourceState {
        switch self {
        case .Loading(let oldData):
            return .Error(Box(error),Box(oldData.value))
        default:
            assert(false, "Invalid state transition to .Error from other than .Loading")
        }
    }

    func toReady(data: D) -> DataSourceState {
        switch self {
        case .Loading:
            return .Ready(Box(data))
        default:
            assert(false, "Invalid state transition to .Ready from other than .Loading")
        }
    }
}

Using these functions to switch state serves three purposes:
- Abstracting away the implementation details of where and how the data is stored in the enum
- Making sure there can be no invalid state transitions
- Enforcing the associated data to be present at the moment you switch to a state

This state machine is actually pretty nice to use now:

class DataSource<T> {
  var state: State<[T]> = .Empty

  func load() {
    state = state.toLoading()
    requestData({ data,error in
      if error != nil {
        self.state = self.state.toError(error)
      } else {
        self.state = self.state.toReady(data)
      }
    })
  }

  func numberOfItems() -> Int {
    return state.data?.count ?? 0
  }
}

This enum could be further extended to have callbacks when changing the state, so it is much easier implement behaviour upon state change. The full code for this post can be found in this gist


  1. The actual enum would look a bit less nice, because of limitations in Swift. You’ll have to box the generic value, but we’ll ignore that for readability in this post