Last year, I stumbled on a blog post called GameplayKit State Machine for non-game apps. I was intrigued. I create finite-state automata all day long; why not make it formal? And Apple has created functionality just for that? Of course we should use it!

And then I looked at GKStateMachine and GKState. They really don’t give you much to work with, I thought. Maybe this approach isn’t really all that great. Let’s try it.

I’m working on an app for a client, and there’s a screen for inviting friends to a game. A game is created, and then if we’ve also created a photo then the photo is added to the game, and if we have a comment, then a comment is added to the photo, and if we’ve selected users to invite to the game, then those invites are sent. But of course, at any point in this sequence a request might fail, and then the view controller needs to know how to fall back and proceed from there. Conceptually it’s not that complicated, but structuring the code was a little complicated, and it seemed like the perfect chance to try GKStateMachine. Of course, it might be nice to batch some or all of these actions into a single request, instead of proceeding serially. But we have the API Gotham needs right now, and not the one it deserves. Or something.

To use GKStateMachine, you create states by subclassing GKState. You initialize the state machine with these subclasses, and state transitions occur by calling enterState. The machine calls (at runtime) isValidNextState on the current state with the requested new state, and if the transition is valid, then the state machine updates.

So, GKState subclasses. My first question was, How much code should I move into the state subclasses? Do the states just manipulate the VC and vice versa, or do I move significant chunks of code from the VC into the states? Do the states contain functionality, or do they just call it? For example, right now my VC has a method for pulling down data from the API. If we enter a LoadingData state, does the state machine call this method, or does the state itself contain the code? If the former, then I’ll have to create a pretty darn comprehensive interface between the state and the subclass. Additionally, I wasn’t sure how to guarantee that the VC would be kept in sync with the machine, and vice versa. If the state is manipulating the VC, then can’t anything manipulate the VC, including the VC itself? Then my VC can do whatever, regardless of the state machine. When I envisioned a very formal and fancy state diagram for my VC, that was not what I had in mind.

So let’s move functionality from the VC directly into the states. This would require making everything in the VC public, which I detest, or we could declare the state subclasses within the VC class itself. This makes for very tight coupling, but I’m not sure if it’s possible to avoid tight coupling between the state machine which controls our view controller, and the view controller itself. This is probably bad for testability, but hold that thought for now. I created the following GKState subclasses in an extension of my view controller:

SelectingRecipientsState

SavingGameState

SavingPhotoState

PostingCommentState

SendingInvitesState

ShowingErrorMessageState

DoneState

CancelledState

As mentioned before, our state diagram is constructed (mostly) at runtime via whatever code is in the isValidNextState method for the current state. In my head, this wasn’t how I wanted things work. Effectively, the state diagram at compile time is a complete graph:

The initial state diagram (and the state diagram for the VC without using GKStateMachine).

and edges may or may not be trimmed at runtime, depending on the logic in our isValidNextState methods. But I wanted to construct at least part of the graph at compile time. After all, we should never go from SendingInvitesState to the SavingGameState state, right? That simply wouldn’t make sense, and shouldn’t be possible. Ever. But how can we simply and easily enforce that fact? How do I keep a simple logic error in isValidNextState from creating a nonsensical state diagram?

I made a base class for my view controller states, and added a default implementation for isValidNextState. I also added a class var possibleNextStates. Our isValidNextState checks if a state is a possible next state, and if so, calls a method runtimeIsValidNextState with that state. Now we can enforce a compile time state diagram, which for our view controller looks like this:

Less complicated.

Some of those edges might still go away at runtime depending on circumstances, but now the possibilities are much more manageable.

So how does this work? In viewDidLoad, the VC enters the SelectingRecipientsState. From there, the app will proceed based on user interaction. If a request fails, the VC enters the ShowingErrorMessageState and displays an alert, and falls back to SelectingRecipientsState after the alert is dismissed. If the user again tries to proceed, the VC will attempt to re-enter whichever state failed previously. If everything works, eventually the VC will enter the DoneState and dismiss; if things fail completely, the VC will enter the FailedState and dismiss. The user can also initiate the CancelledState and dismiss.

It took me some time to wrap my head around this way of thinking (and get what I wanted from Swift), but I think I like it. Being able to “lock up” important chunks of logic into the states, and then calling that logic only by manipulating the state machine, makes the possibilities seem much more finite. In a typical app, it often feels as if any given code path could be executed from any other given code path, and it’s up to the developer to stay out of trouble. If you’re looking at a complicated code base for the first time (or revisiting your own complicated code base after some time has passed), this can be intimidating. Using the state machine, the possible code paths are greatly simplified.

Additionally, visualizing the state diagram using Graphviz was easy, and very helpful. It helped to reason about the behavior of the VC, and avoid nonsensical state transitions. I wish I could visualize code more often.

One thing remains: testing. I’m excited for the possibilities with testing the state diagram or mocking the VC states. But that’s for another post.