Creating a state machine in Swift Nov 13 2019
State machines are used to model systems that can be thought of as a collection of states, and a collection of events that cause state changes. Many of the systems we want to model can be abstracted to a state machine. For example, elements in a Game, devices like vending machines, ATMs, etcetera. In this post, I'll explain what State machines are, and give a simple example of the implementation of a general state machine.
It might be easier if we start with an example.
**Note: You can find the full code on the GitHub Repository.
Table of Contents
Modeling systems with state machines
We are going to use as an example a payphone (I know some readers won't remember this ancient artefact). But I think it is simple and will help us illustrate how state machines work.
Our payphone will have the following states:
- A. Hang up
- B. Waiting for credit
- C. Ready to make a call
- D. On call
- E. Out of credit
Let's work with these states. We need an initial state. In our case, it'll be "A" (hang up), we can now define all the transactions that could make the phone change state:
- Pick up
- Add credit
- Put down
- Use credit
Our state machine also needs a function that will receive the event and based on the current state transition to a new state.
So we have the following elements:
- States: A, B, C, D and E
- Events: 1, 2, 3, and 4
- Function: transaction(CurrentState, Event) -> NewState
Now we can define our valid states and valid events that will take the phone from one state to another. We can represent it using a transaction matrix, where we are going to represent the rows as the states and the columns as the events. Let me show you our transaction matrix:
1
2
3
4
5
6
7
8
9
10
11
12
13
+-----------------------+------------+---------------+-------------+---------------+
| State / Event | 1. Pick up | 2. Add credit | 3. Put down | 4. Use Credit |
+----------------------------------------------------------------------------------+
| A. Hang up | B | | | |
+----------------------------------------------------------------------------------+
| B. Waiting for Credit | | C | A | |
+----------------------------------------------------------------------------------+
| C. Ready for Call | | C | A | D |
+----------------------------------------------------------------------------------+
| D. On call | | | A | E |
+----------------------------------------------------------------------------------+
| E. Out of Credit | | C | A | |
+-----------------------+------------+---------------+-------------+---------------+
What the transaction matrix represents in words is the following:
- The phone is on state A. It can only move to state B via event 1.
- The phone is on state B. It can move to state C via event 2, or state A via event 3.
- The phone is on state C. It can move to state C via event 2 or state A via event 3, or state D via event 4.
- The phone is on state D. It can only move to state A via event 3, or state E via event 4.
- The phone is on state E. It can move to state C via event 2 or state A via event 3.
We could model this system by creating a custom program that handles the states and events of our phone, or we can make use of the sate pattern. This is one of those cases when knowing about design patterns is useful. If you are familiar with the state pattern, we can model our system in terms of states and events (As we just did) and think of it as a Finite State Machine.
A side note on design patterns
When we begin our journey as software developers, we want to get to the "finish line" fast and become software developers. But a good starting point would be the question, what makes us software developers? To me, when we write the first line of code, we become software developers. We can figure everything out from there, after that point is only a matter of experience. We are as human as the people that wrote the first compiler. We can, with enough time and dedication, come up with the same (or even better) solutions as our computer forefathers.
And here is where things get interesting, we can come up with the same solutions to the same old problems every time we encounter a problem, or we can use what our computer-ancestors created. It is faster to learn from what our ancestors created. We can leverage their knowledge and build from there.
Design patterns are one such thing. After creating many projects, some programmers started seeing patterns on to the systems they were trying to model using computers. They find out that some systems behave in the same way so that they can be modelled in the same manner, and design patterns were "discovered".
Imagine modelling a vending machine, then an elevator, then an ATM, and noticing the pattern. All of those systems are a collection of states and a collection of events that make them change to a different state. Fundamentally all of them behave in the same pattern, what changes are the states and what the events are, but the core functionality is still the same. If we solve the problem of State and Events, we can now solve every problem that fits that model. Wouldn't it be great? Well, you know the end of the story, we have state machines, that were based on Finite-State-Machines.
All this to say, design patterns are useful. If you spend some time exploring some of the most common design patterns, you'll find that you already know/have the solution to modelling a lot of the problems you'll encounter. And now it's up to you to find new patterns and combine the existing ones in exciting ways that will be of benefit for future generations.
We, software developers, are a new breed. We have our roots on mathematicians, engineers, and everyday people that wanted to solve problems. Don't forget our roots, solve problems and take advantage of our lineage and don't waste time finding solutions to the same problems that have already been solved, find new ones.
Ok, back to our topic.
Modelling state machines
We can use many techniques to represent a State Machine. I'm going to use the classic approach, using Objects and Inheritance. We use an object to model the generic state machine and another object that represents the generic states. We can define our custom states by inheriting from the generic state.
The generic state machine works as expected, it only keeps track of the current state, and it can change state by an event. Before I get some comment saying "We now have protocols we could have used Protocol Oriented Programming", I know the idea is still the same, and at the end of the post, you'll see why I decided to take this approach.
Our state machine will have the following elements:
- The machine's valid states.
- The ability to handle events.
We are going to start with a basic implementation. Let's start by creating the directory to store our project. Let's call it RDStateMachine
.
1
2
$ mkdir RDStateMachine
$ swift package init --type executable
Now let's create our state machine object, let's create a file inside Sources/RDStateMachine/
called RDStateMachine.swift
, and add the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RDStateMachine {
private(set) var currentState: RDState?
var states: [RDState]
init(states: [RDState]) {
self.states = states
self.currentState = nil
}
func enter(_ state: RDState) -> Bool {
if currentState == nil {
currentState = state
return true
} else if !currentState!.isValidNextState(state) {
return false
}
currentState!.willExit(to: state)
currentState = state
return true
}
}
We initialise an RDStateMachine
with the valid states. The state machine keeps track of the current state and has a function that is in charge of changing to a different state. The function enter
verifies if changing from our current state to the new state is valid. If switching to the new state is valid, it "notifies" the currentState that it will exit to the new state and changes the current state to the new state.
Now let's work on our generic state object. Let's create a new file inside Sources/RDStateMachine/
, call it RDState
and add the following content:
1
2
3
4
5
6
7
8
9
public class RDState {
func isValidNextState(_ state: RDState) -> Bool {
return false
}
func willExit(to state: RDState) {
}
}
It is just a template object. The isValidNextState
allows us to define if moving to a new state is valid or not. The willExit(to:)
function, will enable us to define any logic we want to execute when changing to a new state.
Ok, that would be enough to represent any simple state machine. We can test it by modelling our payphone.
Modeling our payphone
We are going to use the same project to test our generic state machine. The state machine stays the same. The only thing that changes is the logic we define in the states. This means that we need to create our own states and logic. For that, let's create all the states, and override the isValidNextState(_:)
and willExit(to:)
to match our transaction matrix.
1
2
3
4
5
6
7
8
9
10
11
12
13
+-----------------------+------------+---------------+-------------+---------------+
| State / Event | 1. Pick up | 2. Add credit | 3. Put down | 4. Use Credit |
+----------------------------------------------------------------------------------+
| A. Hang up | B | | | |
+----------------------------------------------------------------------------------+
| B. Waiting for Credit | | C | A | |
+----------------------------------------------------------------------------------+
| C. Ready for Call | | C | A | D |
+----------------------------------------------------------------------------------+
| D. On call | | | A | E |
+----------------------------------------------------------------------------------+
| E. Out of Credit | | C | A | |
+-----------------------+------------+---------------+-------------+---------------+
Ok, let's create HangUpState.swift
and add the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class HangUpState: RDState {
override func isValidNextState(_ state: RDState) -> Bool {
switch state {
case is WaitingForCreditState:
return true
default:
print("Wrong action. You can only pickup the phone")
return false
}
}
override func willExit(to state: RDState) {
print("Picking up the phone")
}
}
From State A, we can only go to State B (Waiting for Credit).
Let's create WaitingForCreditState.swift
and add the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class WaitingForCreditState: RDState {
override func isValidNextState(_ state: RDState) -> Bool {
switch state {
case is ReadyForCallState, is HangUpState:
return true
default:
print("Wrong action. You can only add more credit or Hang up")
return false
}
}
override func willExit(to state: RDState) {
if(state is ReadyForCallState) {
print("Ready for Call")
} else if(state is HangUpState) {
print("Hanging up")
}
}
}
From this state, we can only move to state C (Ready for Call) and A (Hang up).
Let's create ReadyForCallState.swift
and add the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ReadyForCallState: RDState {
override func isValidNextState(_ state: RDState) -> Bool {
switch state {
case is ReadyForCallState, is OnCallState, is HangUpState:
return true
default:
print("Wrong action. You can only add more credit, use your credit (make a call), or Hang up")
return false
}
}
override func willExit(to state: RDState) {
if(state is ReadyForCallState) {
print("Ready for Call")
} else if(state is OnCallState) {
print("Making a call")
} else if(state is ReadyForCallState) {
print("Hanging up")
}
}
}
From this state, we can go to state C (Ready for Call), D (On Call), and A (Hang up).
Let's create OnCallState.swift
with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class OnCallState: RDState {
override func isValidNextState(_ state: RDState) -> Bool {
switch state{
case is OutOfCreditState, is HangUpState:
return true
default:
print("Wrong action. You can only use your credit or Hang up")
return false
}
}
override func willExit(to state: RDState) {
if (state is OutOfCreditState) {
print("Ran out of credit")
} else if (state is HangUpState) {
print("Hanging up")
}
}
}
From this state, we can go to state E (Out of credit), and A (Hang up).
Let's create OutOfCreditState.swift
with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class OutOfCreditState: RDState {
override func isValidNextState(_ state: RDState) -> Bool {
switch state {
case is ReadyForCallState, is HangUpState:
return true
default:
print("Wrong action. You can only add more credit or Hang up")
return false
}
}
override func willExit(to state: RDState) {
if(state is ReadyForCallState) {
print("Ready for Call")
} else if(state is HangUpState) {
print("Hanging up")
}
}
}
From this state, we can only move to state C (Ready for Call) and A (Hang up).
And that's it. All of our states are modelled. Let's now create our event logic in our main.swift
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
print("Welcome to our payphone!")
let hangUpState = HangUpState()
let waitingForCreditState = WaitingForCreditState()
let readyForCallState = ReadyForCallState()
let onCallState = OnCallState()
let outOfCreditState = OutOfCreditState()
let sm = RDStateMachine(states: [hangUpState, waitingForCreditState, readyForCallState, onCallState, outOfCreditState] )
sm.enter(hangUpState)
mainLoop: while true {
print("Enter your actions(1. Pick up | 2. Add credit | 3. Put down | 4. Make a call | 5. Exit): ", terminator: "")
let option = readLine(strippingNewline: true)!
switch option {
case "1":
sm.enter(waitingForCreditState)
case "2":
sm.enter(readyForCallState)
case "3":
sm.enter(hangUpState)
case "4":
if sm.currentState is WaitingForCreditState {
sm.enter(onCallState)
} else if sm.currentState is ReadyForCallState {
sm.enter(onCallState)
} else if sm.currentState is OnCallState {
sm.enter(outOfCreditState)
}
case "5":
print("bye bye.")
break mainLoop
default:
print("Invalid option")
}
}
As you can see, we first initialise our states then create a state machine that supports those states, and we are ready to jump from state to state. Run our program and play with changing the states.
1
$ swift run
Using GameplayKit's GKStateMachine
I hope that you find it useful to create our implementation of a state machine. But I have good news we don't have to use our implementation of state machines. If you are on macOS, you can instead use GameplayKit's GKStateMachine
. Our state machine is built based on the basic elements of GKStateMachine, and the logic is the same, we create state objects that inherit from GKState
that will contain the logic of our state.
You can imagine why GameplayKit offers a state machine. In game development, modelling elements like NPCs, players, and environment can easily be done by using the state pattern. I encourage you to have a look at GameplayKit development guide to see more examples.
Ok, that's it for this post.
**Note: You can find the full code on the GitHub Repository.
Final thoughts
This post was a compilation of a few things I wanted to share. Starting with state machines. State machines are so useful and come up very often as a model to represent many systems that having experience with them will help us in many projects.
I also wanted to share some thoughts on design patterns. I sometimes feel that we, as a group, make terms sound much harder and complicated than what they are. I think that design patterns are one of them. Design patterns are nothing but templates that have been used and proven to be useful in many scenarios. It is worth having a look and learning to spot them.
Let me know if you have any comments, and feedback is always welcome.
Related topics/notes of interest
- A good article on finite state machines and regular expressions. If you have heard of Regex and think that they are hard, maybe if you approach them as a Finite State Machine, you'll have an easier time to understand them. Regex is one of my favourite topics, let me know if you want me to write about it.
- Apple's GameKit GKStateMachine documentation.
- Apple's GameplayKit programming Guide.