Stateful View Controller
State Machine
"A finite-state machine (FSM) or finite-state automaton (FSA, plural: automata), finite automaton, or simply a state machine, is a mathematical model of computation. It is an abstract machine that can be in exactly one of a finite number of states at any given time" Wikipedia
Does it play well with view controllers?
It does. With a state machine we are given a way to model and control our execution flow. This makes our code more organised and very easy to debug, as the flow is managed in one place, it makes it easier to understand and anticipate what's coming next.
A tidy ViewController is a good ViewController
The view controller responsibility does literally what it name means. A view controller controls views (There, I've said it). It may sound obvious, but sometime we really don't pay attention to those little things with big meaning, which in our View Controllers world means:
A cluttered not fun to read view controller
A n undebugable (not a real word) view controller
A view controller that will make you stay up late and baffle with things you should not waste your time on.
This is one of those times I tend to disagree with Shia:
Don't
Because he is right 60% of the time, all the time
And instead, put some more time in your view controller to make your life easier.
The power of a state machine
It really is a simple yet powerful concept. Let's dive into details. Here at Sanga, we cannot use SwiftUI just yet as we need to have two iOS versions back backwards compatibility. But we kept that in mind when we designed our StatefulViewController or make it a bit easier for us to transition to SwiftUI in the right time for us.
A state machine is built from:
States
Inputs
Transitions
States
A machine can be in a each of the defines states, for example if we try to model Shia Labouf behaviour we can get the following states:
paying attention
asleep
repeat saying "DO IT" forever
confused
Inputs
The inputs are every thing you can tell Shia to do, for example, you can tell Shia to go to sleep, and Shia will do that only if he is paying attention, because he won't hear you well if he is already asleep or if he keeps shouting repeatedly "DO IT". So telling him to go to sleep if he is not awake and paying attention makes Shia confused. Makes sense, doesn't it?
Transitions
A Transition gives you the next state, based on the previous state + the given input. In our example (Yes, I could've select a Dog and talk about barking and eating, or a Cat which is very loved by the internet and we could talk about mewing and small cats in a cup forever. I tend to find Shia Labouf much more interesting) Shia's brain is where those transitions are being decided, only he knows how to respond to someone telling him to go to sleep. And he does that based on his current state.
Coding time
Let's DO IT. Create a class (I know you will just copy and paste it so go ahead and do that)
Lets start by creating our State enum
enum State: String {
case awake = "Awake and paying attention"
case asleep = "Sleeping..."
case repeatedlySayingDoIt = "Repeatedly saying DO IT"
case confused = "Confused"
}
Lets create our Input enum
enum Input {
case goToSleep
case wakeUp
case payAttention
case sayDoIt
}
Lets setup our transition method
// State
var state: State!
// Transition
func shia(_ input: Input) -> State {
switch (self.state, input) {
case (.awake, .goToSleep):
return .asleep
case (.awake, .sayDoIt):
return .repeatedlySayingDoIt
case (.asleep, .wakeUp):
return .awake
case (.confused, .wakeUp):
return .awake
default:
return .confused
}
}
Lets create a label for Shia
var shiaLabel: UILabel!
// Creating Shia's label
func createShia() {
self.shiaLabel = UILabel(frame: CGRect(x: 0,
y: 0,
width: self.view.bounds.size.width - 10,
height: 20))
self.shiaLabel.center = self.view.center
shiaLabel.textAlignment = .center
self.view.addSubview(shiaLabel)
}
Lets create the setState function, which will take a state and will render it to the screen after the given delay, which is the value of stateRenderDelay. (The setState function is the place to run the logic for each state.)
// UI
var stateRenderDelay = 0
func setState(_ state: State) {
self.state = state
DispatchQueue.main.asyncAfter(deadline: .now() + stateRenderDelay) {
self.shiaLabel.text = state.rawValue
}
stateRenderDelay += 2 // set a different activation time for each state
}
Set state will show the text that is relevant to the given state for 2 seconds.
Our viewDidLoad may look like
// DO IT
override func viewDidLoad() {
self.view.backgroundColor = .white
// DO IT
createShia()
// Set Initial state
// Note that here we set the state and not triggering an input
setState(.asleep) // shia is sleeping
setState(shia(.wakeUp)) // shia is awake
setState(shia(.goToSleep)) // shia is sleeping
setState(shia(.wakeUp)) // shia is awake
setState(shia(.sayDoIt)) // shia is saying DO IT
setState(shia(.sayDoIt)) // shia is confused
setState(shia(.wakeUp)) // shia is awake
}
Going deeper
Going deeper, if needed, we can use Swift's 5 Result type (a simple completion block will also do just fine) and make transition do some async stuff like fetching remote data before calling the completion block with the next step.