Android Fall Sale

Get up to 55% off all Android books!

Practical State Machines with GameplayKit

In this tutorial, you’ll convert an iOS app to use a state machine for navigation logic using GameplayKit’s GKStateMachine.

3.7/5 3 Ratings

Version

  • Swift 5, iOS 13, Xcode 11

Managing state is hard, but you have many techniques to manage state in a regular iOS app. The GameplayKit framework hides one useful technique: GKStateMachine.

State machines are often used in game programming. However, their utilities don’t end there. Programmers have solved problems with state machines for ages. GKStateMachine lives within a framework for game development, but don’t let that stop you. There’s no reason you can’t use it inside any other iOS app that has state to manage.

Some of the uses of state machines could be to make sense of complicated business logic, to control the state of a view controller or to manage navigation. In this tutorial, you’ll refactor an app to use a state machine to control navigation.

By the time you’re done with this tutorial, you’ll gain a deeper understanding of state machines and learn how to:

  • Simplify the logic in an app with a state machine.
  • Use GKStateMachine to configure your state machine.
  • Control the flow of your state machine by defining valid transitions.

Getting Started

To start off, download the sample project using the Download Materials button at the top or bottom of this tutorial. Kanji List is an app for learning Kanji characters from the Japanese language that’ll surely be indispensable for your next trip to Tokyo. The app is from the RayWenderlich Coordinator Tutorial for iOS.

The app shows a list of kanji. Clicking on a kanji will show you the the meaning of that kanji and a list of words that use the kanji. Clicking on any of those words will take you to a list of kanji in the word, where you can repeat the process to your heart’s content.

Kanji List functionality

In this tutorial, you’ll refactor the navigation logic of Kanji List to make use of a state machine. Currently, Kanji List uses the coordinator pattern for navigation. This is great; it means that the navigation code has already been extracted from the view controllers. You’ll only need to add a state machine to organize the coordinators.

If you’re unfamiliar with the coordinator pattern, don’t worry. It’s a simple pattern for handling an app’s flow between view controllers. If you want to learn more, take a quick look at the Coordinator Tutorial for iOS.

Understanding State Machines

A state machine is a mathematical abstraction for representing a system that can be in only one state at a time. Sounds complicated, right? It’s actually very simple and a very useful way to look at some problems. Think about the battery in an iPhone, for example. You can look at a battery as a state machine:

Battery State Machine

This state machine has four states:

  1. Charging: The phone is plugged in and charging.
  2. Fully Charged: The battery is full and the phone is receiving power from the charger.
  3. Discharging: The phone is unplugged and relying on the battery for power.
  4. Flat: The battery has run out of charge.

Plugging the phone into a charger puts it into the Charging state. From there, it can transition to the Discharging state when unplugged. If you leave the phone plugged in until the battery reaches 100%, it will enter the Fully Charged state. When you unplug the phone, if you let the battery discharge fully, it’ll enter the Flat state.

The state machine represents which state transitions are valid. A battery doesn’t go flat while it’s charging (unless you need a new battery!), so there is no transition from Charging to Flat in the state machine.

Different Between States

First, you need to define the different states to represent the navigation in Kanji List as a state machine. Once you’ve opened the starter project in Xcode, build and run the app.

The app starts off with a list of all the supported kanji. This will be the All state. Next, tap on a kanji to show the Detail screen. This will be the Detail state. Finally, tap on one of the words that use the kanji to go to a list of words in the kanji, which you will call the List state.

Kanji List States

Great! Since Kanji List is a simple app, these three states are enough to represent the functionality of the app. As the app grows in functionality, the state machine will be a helpful tool to keep things under control.

Transitioning Between States

Defining the different states in an app helps you to better understand the way the app works. But, without defining the valid transitions between states, the state machine is incomplete.

Each screen contains an All button in the navigation bar to transition to the All screen. That means a transition from the Detail screen or the List screen to the All screen is valid.

Tapping a word takes the app to the List screen, which shows all the kanji in a word. So, you can only transition to the List screen by tapping on a word via the Detail screen.

Additionally, tapping on a kanji navigates to the Detail screen. This means that a transition from the All screen or List screen to the Detail screen is valid.

Tying that all together presents this diagram of the state machine:

Kanji List State Machine

Creating the State Machine

Now, that’s enough talking about states. It’s time to write some code! First, create a file named KanjiStateMachine.swift under the State Machine group. Replace the import statement with this:

import UIKit
import GameplayKit.GKStateMachine

class KanjiStateMachine: GKStateMachine {
  let presenter: UINavigationController
  let kanjiStorage: KanjiStorage

  init(presenter: UINavigationController,
       kanjiStorage: KanjiStorage,
       states: [GKState]) {
    // 1
    self.presenter = presenter

    // 2
    self.kanjiStorage = kanjiStorage

    // 3
    super.init(states: states)
  }
}

While it isn’t required to subclass GKStateMachine, this subclass lets you store some important data that your states will need later. The initializer is a simple one, but here’s what’s going on:

  1. The app’s coordinator pattern uses a UINavigationController as a presenter for view controllers. The state machine will own the presenter.
  2. The KanjiStorage class is essentially the app’s dictionary. It stores all the kanji and the words that contain them. The KanjiStateMachine manages the KanjiStorage object for each state to use.
  3. GKStateMachine‘s initializer needs instances of each state you’re using, so pass it up to GKStateMachine.init(states:).

Next, open ApplicationCoordinator.swift. This is the root coordinator of the application that creates the root view controller and adds it to the app’s UIWindow. Add a new property for the state machine at the top of the class:

let stateMachine: KanjiStateMachine

At the end of init(window:), add the following to create the state machine:

stateMachine = KanjiStateMachine(
  presenter: rootViewController,
  kanjiStorage: kanjiStorage,
  states: [])

Because you haven’t created any GKState classes, you’ll just pass an empty array for the states for the time being.

Creating Each State

Now that you’ve created your state machine, it’s time to add some states. Right now, each coordinator in the app will create other coordinators as needed to navigate to different screens. So, because you want to move the decision of which screen to navigate to, you need to remove that logic from the coordinators.

Start with the ApplicationCoordinator. This class will keep the state machine, but all other coordinators will be created by one of the states in the state machine. So, delete the allKanjiListCoordinator property on the ApplicationCoordinator. You’ll recreate it later inside the AllState class. Remove this line inside init(window:) that creates the coordinator:

allKanjiListCoordinator = KanjiListCoordinator(
  presenter: rootViewController, 
  kanjiStorage: kanjiStorage,
  list: kanjiStorage.allKanji(), 
  title: "Kanji List")

As well as this method inside start() that fires the coordinator:

allKanjiListCoordinator.start()

Build and run the app. Seems like it’s lost some of its utility:

Broken Kanji List

You’ll gain it back when you start creating the states.

All State

Add a new file named AllState.swift under the State Machine group. Replace its import statement with this:

import GameplayKit.GKState

class AllState: GKState {
  // 1
  lazy var allKanjiListCoordinator = makeAllKanjiCoordinator()

  // 2
  override func didEnter(from previousState: GKState?) {
    allKanjiListCoordinator?.start()
  }

  private func makeAllKanjiCoordinator() -> KanjiListCoordinator? {
    // 3
    guard let kanjiStateMachine = stateMachine as? KanjiStateMachine else {
      return nil
    }
    
    let kanjiStorage = kanjiStateMachine.kanjiStorage

    // 4
    return KanjiListCoordinator(
      presenter: kanjiStateMachine.presenter,
      kanjiStorage: kanjiStorage,
      list: kanjiStorage.allKanji(),
      title: "Kanji List")
  }
}

Here’s what’s going on:

  1. Here, you recreate the coordinator you removed from ApplicationCoordinator.swift.
  2. didEnter(from:) fires whenever the state machine enters a new state. It’s the ideal place to fire off the allKanjiListCoordinator to navigate to the All screen.
  3. You can use the stateMachine property on a GKState to get its state machine. Here, you cast it to KanjiStateMachine to access the properties you added earlier.
  4. To build a KanjiListCoordinator, give it all the data needed to present the All screen.

Next, open ApplicationCoordinator.swift and find the line where you create the state machine inside init(window:). Create an instance of the AllState and pass it into the states array, like this:

stateMachine = KanjiStateMachine(
  presenter: rootViewController,
  kanjiStorage: kanjiStorage,
  states: [AllState()])

In start(), add the following line to the beginning of the method:

stateMachine.enter(AllState.self)

This causes the state machine to enter the AllState and triggers the allKanjiListCoordinator to navigate to the All screen. Build and run the app. Everything’s working smoothly again!

All Kanji Screen

Detail State

Open KanjiListCoordinator.swift and find kanjiListViewController(_:didSelectKanji:) in the extension at the bottom. This method creates and starts a KanjiDetailCoordinator, causing the app to navigate to the Detail screen. Remove the contents of the method, leaving it empty.

Build and run the app. It should still show the All screen. But, because you removed the navigation logic from kanjiListViewController(_:didSelectKanji:), the KanjiListCoordinator won’t create the next coordinator to move to a different screen. Tapping on a kanji does nothing.

To fix this, you need to add the code you just removed to a new state object. Add a new file named DetailState.swift under the State Machine group. Replace its import statement with this:

import GameplayKit.GKState

class DetailState: GKState {
  // 1
  var kanji: Kanji?
  var kanjiDetailCoordinator: KanjiDetailCoordinator?

  override func didEnter(from previousState: GKState?) {
    guard 
      let kanji = kanji,
      let kanjiStateMachine = (stateMachine as? KanjiStateMachine) 
      else {
        return
    }
    
    // 2
    let kanjiDetailCoordinator = KanjiDetailCoordinator(
      presenter: kanjiStateMachine.presenter,
      kanji: kanji,
      kanjiStorage: kanjiStateMachine.kanjiStorage)

    self.kanjiDetailCoordinator = kanjiDetailCoordinator
    kanjiDetailCoordinator.start()
  }
}

Here’s what’s happening:

  1. The KanjiDetailCoordinator needs a Kanji to show the Detail screen. You’ll need to set it here.
  2. Create and start the KanjiDetailCoordinator, similar to what you did previously in kanjiListViewController(_:didSelectKanji:).

Communicating to the State Machine

You need a way to communicate to the state machine that it needs to enter the DetailState. So, you’ll make use of NotificationCenter to submit a notification, and then listen for it inside ApplicationCoordinator. Back in KanjiListCoordinator.swift, add this line to kanjiListViewController(_:didSelectKanji:):

NotificationCenter.default
  .post(name: Notifications.KanjiDetail, object: selectedKanji)

Notifications.KanjiDetail is just an NSNotification.Name object that was created for you ahead of time. This posts a notification, passing the selectedKanji that’s needed to show the Detail screen.

Open ApplicationCoordinator.swift again. Go to the line where you create the state machine inside init(window:). Create an instance of the DetailState and pass it into the states array, like you did for the AllState earlier:

stateMachine = KanjiStateMachine(
  presenter: rootViewController,
  kanjiStorage: kanjiStorage,
  states: [AllState(), DetailState()])

Next, add this method:

@objc func receivedKanjiDetailNotification(notification: NSNotification) {
  // 1
  guard 
    let kanji = notification.object as? Kanji,
    // 2
    let detailState = stateMachine.state(forClass: DetailState.self) 
    else {
      return
  }

  // 3
  detailState.kanji = kanji

  // 4
  stateMachine.enter(DetailState.self)
}

Here’s what’s going on:

  1. Get the Kanji object passed with the notification
  2. GKStateMachine.state(forClass:) returns the instance of a state that you passed into the state machine’s initializer. Get that instance here.
  3. Store the kanji for the DetailState to use when creating its KanjiDetailCoordinator.
  4. Finally, enter the DetailState, which will create and start the KanjiDetailCoordinator.

You still need to subscribe to the KanjiDetail notification, so add this to subscribeToNotifications():

NotificationCenter.default.addObserver(
  self, selector: #selector(receivedKanjiDetailNotification),
  name: Notifications.KanjiDetail, object: nil)

Build and run the app. You should be able to tap on a Kanji and reach the Detail screen again.

List State

The process for implementing the ListState will be similar to what you’ve seen before. You’ll remove navigation logic from a coordinator, move it to a new GKState class and communicate to the stateMachine that it should enter the new state.

To start off, open KanjiDetailCoordinator.swift. kanjiDetailViewController(_:didSelectWord:) fires when the user taps on a word on the Detail screen. It then creates and starts a KanjiListCoordinator to show the List screen for all the kanji in that word.

Remove the contents of kanjiDetailViewController(_:didSelectWord:) and replace it with this:

NotificationCenter.default.post(name: Notifications.KanjiList, object: word)

Back in ApplicationCoordinator.swift, create a new empty method to receive the notification:

@objc func receivedKanjiListNotification(notification: NSNotification) {
}

Then, add the following code to subscribe to the notification in subscribeToNotifications().

NotificationCenter.default.addObserver(
  self, selector: #selector(receivedKanjiListNotification),
  name: Notifications.KanjiList, object: nil)

Under the State Machine group, create a new file named ListState.swift. Replace its import statement with this:

import GameplayKit.GKState

class ListState: GKState {
  // 1
  var word: String?
  var kanjiListCoordinator: KanjiListCoordinator?

  override func didEnter(from previousState: GKState?) {
    guard 
      let word = word,
      let kanjiStateMachine = (stateMachine as? KanjiStateMachine) 
      else {
        return
    }
    
    let kanjiStorage = kanjiStateMachine.kanjiStorage

    // 2
    let kanjiForWord = kanjiStorage.kanjiForWord(word)

    // 3
    let kanjiListCoordinator = KanjiListCoordinator(
      presenter: kanjiStateMachine.presenter, kanjiStorage: kanjiStorage,
      list: kanjiForWord, title: word)

    self.kanjiListCoordinator = kanjiListCoordinator
    kanjiListCoordinator.start()
  }
}

It’s the same pattern that you used for the DetailState, but here’s what’s going on:

  1. The List screen shows all kanji in a word. So, store that word here for yourself to get the kanji from.
  2. Use the KanjiStorage object to get a list of kanji from the word.
  3. Pass all the necessary data into the initializer for KanjiListCoordinator and call start() to navigate to the List screen.

Now that you have a ListState, you can pass it into the state machine and enter the state when needed. Back in ApplicationCoordinator.swift, pass an instance of the ListState to the KanjiStateMachine‘s initializer in init(window:):

stateMachine = KanjiStateMachine(
  presenter: rootViewController,
  kanjiStorage: kanjiStorage,
  states: [AllState(), DetailState(), ListState()])

Add the following to receivedKanjiListNotification(notification:) to configure and enter the ListState:

// 1
guard 
  let word = notification.object as? String,
  let listState = stateMachine.state(forClass: ListState.self) 
  else {
    return
}

// 2
listState.word = word

// 3
stateMachine.enter(ListState.self)

Here’s the breakdown:

  1. Get the word from the notification and the ListState instance from the state machine.
  2. Set the word on the ListState for the state to configure the KanjiListCoordinator.
  3. Enter the ListState, causing the KanjiListCoordinator to start the navigation to the List Screen.

Build and run the app. Everything should be working smoothly, all neatly managed by the GKStateMachine :]

Kanji List Functionality Restored

Using Other Abilities of GKStateMachine

Remember the different transitions between states of the state machine?

Kanji List State Transitions

Well, you can add these transitions to your GKState classes to prevent any invalid transitions from ever happening. Open AllState.swift and add the following method:

override func isValidNextState(_ stateClass: AnyClass) -> Bool {
  return false
}

isValidNextState(_:) lets you define which states that this GKState can reach. Because it returns false, the state machine won’t be able to transition from this state to any others. Build and run the app. Tapping on a kanji does nothing:

Unable to transition between states

Because it only makes sense for the AllState to move to the Detail Screen for a particular kanji, the only valid next state is the DetailState. Replace the contents of isValidNextState(_:) with this:

return stateClass == DetailState.self

Build and run the app, and you should be able to reach the Detail screen again. Next, add this to DetailState.swift:

override func isValidNextState(_ stateClass: AnyClass) -> Bool {
  return stateClass == AllState.self || stateClass == ListState.self
}

The DetailState can move to either of the remaining states, so you return true for either state.

Similar to the DetailState, add the following to ListState.swift:

override func isValidNextState(_ stateClass: AnyClass) -> Bool {
  return stateClass == DetailState.self || stateClass == AllState.self
}

Build and run the app. Everything should still be functional.

Now, there’s one final change to make for the AllState. Open ApplicationCoordinator.swift and take a look at receivedAllKanjiNotification().

When the All button in the navigation bar is tapped, it fires a notification and the ApplicationCoordinator pops to the root view controller. The coordinator shouldn’t have this sort of knowledge about the navigation hierarchy. All it should know is that the app is meant to enter the AllState. So, remove the contents of receivedAllKanjiNotification() and replace it with this:

stateMachine.enter(AllState.self)

Now, rather than directly popping to the root view controller, receivedAllKanjiNotification() will just transition to the AllState instead. Build and run the app. When you tap the All button, it pushes a new view controller onto the stack. Rather than pushing a new view controller, you still want it to pop to the root view controller. Open AllState.swift and replace the contents of didEnter(from:) with this:

if previousState == nil {
  allKanjiListCoordinator?.start()
} else {
  (stateMachine as? KanjiStateMachine)?.presenter
    .popToRootViewController(animated: true)
}

When you call GKStateMachine.enter(_:), the previous state gets passed into didEnter(from:) in for the current state. If this is the first state for the state machine, there is no previous state, so previousState will be nil. In this case, you can call start() on the allKanjiListCoordinator. But if there is a previous state, it means you should pop to the root view controller to get back to the All screen.

Build and run the app. When on the List screen or Detail screen, the All button should take you back to the All screen.

That’s it. You’ve refactored Kanji List to use a GKStateMachine to manage the navigation in the app. Great work!

Where to Go From Here?

If you want to download the finished project, use the Download Materials button at the top or bottom of this tutorial.

Kanji List is still a simple app, but in this tutorial, you learned how to use a state machine combined with the coordinator pattern to handle the navigation in a clear and defined way. With this approach, no class needs to know how the navigation is done or what view controllers are used — simply instruct the GKStateMachine to transition to the appropriate state.

I hope you enjoyed learning about a different pattern to manage navigation in this tutorial. Maybe you can find some other uses for a GKStateMachine in the apps you’re building? If you’re interested in learning about clean architectures for your iOS apps, take a look at the Advanced iOS App Architecture book for more.

Don’t forget to take a look at the Coordinator Tutorial for iOS for more info on the coordinator pattern. As always, Apple’s documentation on GKState and GKStateMachine is invaluable. You might also be interested in learning more about state machines in general.

If you have any questions or insights to share, I’d love to hear from you in the comments below. :]

Average Rating

3.7/5

Add a rating for this content

3 ratings

Contributors

Comments