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. By Keegan Rush.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Practical State Machines with GameplayKit
25 mins
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:
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:
- Here, you recreate the coordinator you removed from ApplicationCoordinator.swift.
-
didEnter(from:)
fires whenever the state machine enters a new state. It’s the ideal place to fire off theallKanjiListCoordinator
to navigate to the All screen. - You can use the
stateMachine
property on aGKState
to get its state machine. Here, you cast it toKanjiStateMachine
to access the properties you added earlier. - 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!
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:
- The
KanjiDetailCoordinator
needs aKanji
to show the Detail screen. You’ll need to set it here. - Create and start the
KanjiDetailCoordinator
, similar to what you did previously inkanjiListViewController(_: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:
- Get the
Kanji
object passed with the notification -
GKStateMachine.state(forClass:)
returns the instance of a state that you passed into the state machine’s initializer. Get that instance here. - Store the kanji for the
DetailState
to use when creating itsKanjiDetailCoordinator
. - Finally, enter the
DetailState
, which will create and start theKanjiDetailCoordinator
.
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:
- The List screen shows all kanji in a word. So, store that word here for yourself to get the kanji from.
- Use the
KanjiStorage
object to get a list of kanji from the word. - Pass all the necessary data into the initializer for
KanjiListCoordinator
and callstart()
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:
- Get the word from the notification and the
ListState
instance from the state machine. - Set the word on the
ListState
for the state to configure theKanjiListCoordinator
. - Enter the
ListState
, causing theKanjiListCoordinator
to start the navigation to the List Screen.
Build and run the app. Everything should be working smoothly, all neatly managed by the GKStateMachine
:]