• waffleman

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:

  1. A cluttered not fun to read view controller

  2. A n undebugable (not a real word) view controller

  3. 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:

  1. paying attention

  2. asleep

  3. repeat saying "DO IT" forever

  4. 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.

8 views