版本记录
| 版本号 | 时间 |
|---|---|
| V1.0 | 2019.08.21 星期三 |
前言
GameplayKit框架,构建和组织你的游戏逻辑。 整合常见的游戏行为,如随机数生成,人工智能,寻路和代理行为。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. GameplayKit框架详细解析(一) —— 基本概览(一)
开始
首先看下主要内容
在本教程中,您将使用
GameplayKit的GKStateMachine将iOS应用程序转换为使用状态机进行导航逻辑。
下面看下写作环境
Swift 5, iOS 13, Xcode 11
管理状态很难,但是您可以使用许多技术来管理常规iOS应用中的状态。 GameplayKit框架隐藏了一种有用的技术:GKStateMachine。
状态机通常用于游戏编程。但是,它们的实用性并不止于此。程序员已经解决了状态机的问题。 GKStateMachine存在一个游戏开发框架内,但不要让这一点阻止你。没有理由你不能在任何其他有状态管理的iOS应用程序中使用它。
状态机的一些用途可能是理解复杂的业务逻辑,控制视图控制器的状态或管理导航。在本教程中,您将重构应用程序以使用状态机来控制导航。
当您完成本教程时,您将更深入地了解状态机并学习如何:
- 使用状态机简化应用程序中的逻辑。
- 使用
GKStateMachine配置状态机。 - 通过定义有效转换来控制状态机流。
首先,打开示例项目。Kanji List是一个学习日语Kanji字符的应用程序,这对你下次去东京旅行肯定是不可或缺的。
该应用程序显示了Kanji列表。单击Kanji将显示该Kanji的含义以及使用Kanji的单词列表。单击任何一个单词将带您进入单词中的Kanji列表,您可以在其中重复该过程到您的内容。

在本教程中,您将重构Kanji List的导航逻辑以使用状态机。 目前,Kanji List使用协调器模式(coordinator pattern)进行导航。 这很棒;这意味着导航代码已经从视图控制器中提取出来。 您只需要添加状态机来组织coordinators。
如果您不熟悉协调器模式,请不要担心。 这是一个简单的模式,用于处理视图控制器之间的应用程序流。
Understanding State Machines
状态机是用于表示一次只能处于一个状态的系统的数学抽象。 听起来很复杂吧? 它实际上非常简单,也是一种非常有用的方法来查看一些问题。 例如,想想iPhone中的电池。 您可以将电池看作状态机:

这个状态机有四种状态:
- 1)Charging:手机已插入并处于充电状态。
- 2)Fully Charged:电池已满,手机正在通过充电器供电。
- 3)Discharging:手机已拔下,依靠电池供电。
- 4)Flat:电池电量不足。
将手机插入充电器会使其进入Charging状态。 从那里,它可以在拔出时转换到Discharging状态。 如果您将电话插入电池直到电池达到100%,它将进入Fully Charged状态。 拔下电话时,如果让电池完全放电,它将进入Flat状态。
状态机表示哪些状态转换有效。 电池在充电时不会flat(除非您需要新电池!),因此状态机中没有从Charging到Flat的转换。
Different Between States
首先,您需要定义不同的状态,以将Kanji列表中的导航表示为状态机。 在Xcode中打开入门项目后,构建并运行应用程序。
该应用程序以所有支持的Kanji列表开始。 这将是All状态。 接下来,点击Kanji以显示详细信息屏幕。 这将是Detail状态。 最后,点击使用Kanji的一个单词转到Kanji中的单词列表,您将其称为List状态。

很好!由于Kanji List是一个简单的应用程序,这三个状态足以代表应用程序的功能。随着应用程序功能的增长,状态机将成为控制事物的有用工具。
1. Transitioning Between States
在应用程序中定义不同的状态有助于您更好地理解应用程序的工作方式。但是,如果没有定义状态之间的有效转换,状态机就不完整了。
每个屏幕都包含导航栏中的All按钮,以转换到All屏幕。这意味着从Detail信息屏幕或List屏幕到All屏幕的转换有效。
点击一个单词可将应用程序带到List屏幕,该屏幕显示单词中的所有kanji。因此,您只能通过Detail屏幕点击单词转换到List屏幕。
此外,点击kanji导航到Detail屏幕。这意味着从All屏幕或List屏幕到Detail屏幕的转换有效。
将所有这些结合起来呈现状态机的这个图:

Creating the State Machine
现在,关于状态已经讨论很多了。 是时候写一些代码了! 首先,在State Machine组下创建一个名为KanjiStateMachine.swift的文件。 用以下内容替换import语句:
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)
}
}
虽然不需要继承GKStateMachine,但是这个子类允许您存储状态稍后需要的一些重要数据。 初始化程序很简单,下面是正在发生的事情:
- 1) 应用程序的协调器模式使用
UINavigationController作为视图控制器的演示者(presenter)。 状态机将拥有演示者。 - 2)
KanjiStorage类本质上是应用程序的字典。 它存储所有kanji和包含它们的单词。KanjiStateMachine管理要使用的每个状态的KanjiStorage对象。 - 3)
GKStateMachine的初始化程序需要您正在使用的每个状态的实例,因此将其传递给GKStateMachine.init(states:)。
接下来,打开ApplicationCoordinator.swift。 这是应用程序的根协调器,它创建根视图控制器并将其添加到应用程序的UIWindow。 在类的顶部为状态机添加新属性:
let stateMachine: KanjiStateMachine
在init(window :)结束时,添加以下内容以创建状态机:
stateMachine = KanjiStateMachine(
presenter: rootViewController,
kanjiStorage: kanjiStorage,
states: [])
因为您还没有创建任何GKState类,所以您只需暂时为状态传递一个空数组。
Creating Each State
现在您已经创建了状态机,现在是时候添加一些状态了。 现在,应用程序中的每个协调员都会根据需要创建其他协调器以导航到不同的屏幕。 因此,因为您想要移动决定导航到哪个屏幕,所以您需要从协调器中删除该逻辑。
从ApplicationCoordinator开始。 此类将保留状态机,但所有其他协调器将由状态机中的某个状态创建。 因此,删除ApplicationCoordinator上的allKanjiListCoordinator属性。 稍后您将在AllState类中重新创建它。 删除创建协调器的init(window :)中的这一行:
allKanjiListCoordinator = KanjiListCoordinator(
presenter: rootViewController,
kanjiStorage: kanjiStorage,
list: kanjiStorage.allKanji(),
title: "Kanji List")
还有启动协调器的start()内部的这个方法:
allKanjiListCoordinator.start()
构建并运行应用程序。 好像它失去了一些功能:

当你开始创建状态时,你将获得它。
1. All State
在State Machine组下添加名为AllState.swift的新文件。 用以下内容替换其import语句:
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")
}
}
下面进行细分:
- 1) 在这里,您重新创建从
ApplicationCoordinator.swift中删除的协调器。 - 2) 只要状态机进入新状态,
didEnter(from :)就会触发。 它是触发allKanjiListCoordinator导航到All屏幕的理想场所。 - 3) 您可以使用
GKState上的stateMachine属性来获取其状态机。 在这里,您将其强制转换为KanjiStateMachine以访问您之前添加的属性。 - 4) 要构建
KanjiListCoordinator,请为其提供显示All屏幕所需的所有数据。
接下来,打开ApplicationCoordinator.swift并找到在init(window :)中创建状态机的行。 创建一个AllState实例并将其传递给states数组,如下所示:
stateMachine = KanjiStateMachine(
presenter: rootViewController,
kanjiStorage: kanjiStorage,
states: [AllState()])
在start()中,将以下行添加到方法的开头:
stateMachine.enter(AllState.self)
这会导致状态机进入AllState并触发allKanjiListCoordinator导航到All屏幕。 构建并运行应用程序。 一切都在顺利进行!

2. Detail State
打开KanjiListCoordinator.swift并在底部的扩展中找到kanjiListViewController(_:didSelectKanji :)。 此方法创建并启动KanjiDetailCoordinator,使应用程序导航到Detail屏幕。 删除方法的内容,将其留空。
构建并运行应用程序。 它应该仍然显示All屏幕。 但是,因为您从kanjiListViewController(_:didSelectKanji :)中删除了导航逻辑,所以KanjiListCoordinator不会创建下一个协调器来移动到不同的屏幕。 点击一个kanji什么也没做。
要解决此问题,您需要将刚刚删除的代码添加到新的状态对象中。 在State Machine组下添加名为DetailState.swift的新文件。 用以下内容替换其import语句:
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()
}
}
下面进行细分
- 1)
KanjiDetailCoordinator需要一个Kanji来显示Detail屏幕。 你需要在这里设置它。 - 2) 创建并启动
KanjiDetailCoordinator,类似于之前在kanjiListViewController(_:didSelectKanji :)中的操作。
3. Communicating to the State Machine
您需要一种与进入DetailState所需的状态机进行通信的方法。 因此,您将使用NotificationCenter提交通知,然后在ApplicationCoordinator中监听它。 回到KanjiListCoordinator.swift,将此行添加到kanjiListViewController(_:didSelectKanji :):
NotificationCenter.default
.post(name: Notifications.KanjiDetail, object: selectedKanji)
Notifications.KanjiDetail只是提前为您创建的NSNotification.Name对象。 这会发布通知,传递显示Detail屏幕所需的selectedKanji。
再次打开ApplicationCoordinator.swift。 转到在init(window :)中创建状态机的行。 创建一个DetailState实例并将其传递给states数组,就像之前为AllState所做的那样:
stateMachine = KanjiStateMachine(
presenter: rootViewController,
kanjiStorage: kanjiStorage,
states: [AllState(), DetailState()])
下面,添加这一行
@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)
}
下面进行细分:
- 1) 获取随通知
notification一起传递的Kanji对象 - 2)
GKStateMachine.state(forClass :)返回传递给状态机初始值设定项的状态实例。 在这里获取该实例。 - 3) 存储在创建其
KanjiDetailCoordinator时要使用的DetailState的kanji。 - 4) 最后,输入
DetailState,它将创建并启动KanjiDetailCoordinator。
您仍然需要订阅KanjiDetail通知,因此将其添加到subscribeToNotifications():
NotificationCenter.default.addObserver(
self, selector: #selector(receivedKanjiDetailNotification),
name: Notifications.KanjiDetail, object: nil)
构建并运行应用程序。 您应该可以点击一个Kanji并再次到达详细信息Detail屏幕。
4. List State
实现ListState的过程与您之前看到的类似。 您将从协调器中删除导航逻辑,将其移动到新的GKState类并与stateMachine通信它应该进入新状态。
首先,打开KanjiDetailCoordinator.swift。 当用户点击详细信息屏幕上的单词时,会触发kanjiDetailViewController(_:didSelectWord :)。 然后,它创建并启动一个KanjiListCoordinator,以显示该单词中所有汉字的列表屏幕。
删除kanjiDetailViewController(_:didSelectWord :)的内容并将其替换为:
NotificationCenter.default.post(name: Notifications.KanjiList, object: word)
回到ApplicationCoordinator.swift,创建一个新的空方法来接收通知:
@objc func receivedKanjiListNotification(notification: NSNotification) {
}
然后,添加以下代码以订阅subscribeToNotifications()中的通知。
NotificationCenter.default.addObserver(
self, selector: #selector(receivedKanjiListNotification),
name: Notifications.KanjiList, object: nil)
在State Machine组下,创建一个名为ListState.swift的新文件。 用以下内容替换其import语句:
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()
}
}
它与您用于DetailState的模式相同,但这是正在发生的事情:
- 1) 列表屏幕显示单词中的所有
kanji。 所以,在这里存储这个词,以便从中获取kanji。 - 2) 使用
KanjiStorage对象从单词中获取kanji列表。 - 3) 将所有必要的数据传递到
KanjiListCoordinator的初始化程序中,并调用start()导航到List屏幕。
现在您已经拥有了ListState,您可以将其传递到状态机并在需要时进入状态。 回到ApplicationCoordinator.swift,在init(window :)中将ListState的实例传递给KanjiStateMachine的初始化器:
stateMachine = KanjiStateMachine(
presenter: rootViewController,
kanjiStorage: kanjiStorage,
states: [AllState(), DetailState(), ListState()])
将以下内容添加到receivedKanjiListNotification(notification :)以配置并输入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)
这是细分:
- 1) 从通知和状态机中的
ListState实例获取单词。 - 2) 在
ListState上设置状态以配置KanjiListCoordinator。 - 3) 输入
ListState,使KanjiListCoordinator开始导航到列表屏幕。
构建并运行应用程序。 一切都应该顺利进行,全部由GKStateMachine管理。

Using Other Abilities of GKStateMachine
还记得状态机状态之间的不同转换吗?

好吧,您可以将这些转换添加到GKState类,以防止任何无效转换发生。 打开AllState.swift并添加以下方法:
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return false
}
isValidNextState(_ :)允许您定义此GKState可以达到的状态。 因为它返回false,状态机将无法从此状态转换到任何其他状态。 构建并运行应用程序。 点击kanji什么也不做:

因为只有将AllState移动到特定kanji的详细信息屏幕才有意义,唯一有效的下一个状态是DetailState。 用以下内容替换isValidNextState(_ :)的内容:
return stateClass == DetailState.self
构建并运行应用程序,您应该能够再次访问详细信息屏幕。 接下来,将其添加到DetailState.swift:
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass == AllState.self || stateClass == ListState.self
}
DetailState可以移动到其余状态中的任何一个,因此对于任一状态都返回true。
与DetailState类似,将以下内容添加到ListState.swift:
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass == DetailState.self || stateClass == AllState.self
}
构建并运行应用程序。 一切都应该仍然有用。
现在,对AllState进行最后一次更改。 打开ApplicationCoordinator.swift并查看receivedAllKanjiNotification()。
当点击导航栏中的All按钮时,它会触发通知,ApplicationCoordinator会弹出到根视图控制器。 协调器不应该具有关于导航层次结构的这种知识。 它应该知道的是该应用程序是为了进入AllState。 因此,删除receivedAllKanjiNotification()的内容并将其替换为:
stateMachine.enter(AllState.self)
现在,不是直接弹出到根视图控制器,receiveAllKanjiNotification()将只转换到AllState。 构建并运行应用程序。 点击All按钮时,它会将新的视图控制器推入堆栈。 您仍然希望它pop到根视图控制器,而不是push到新的视图控制器。 打开AllState.swift并用以下内容替换didEnter(from :)的内容:
if previousState == nil {
allKanjiListCoordinator?.start()
} else {
(stateMachine as? KanjiStateMachine)?.presenter
.popToRootViewController(animated: true)
}
当您调用GKStateMachine.enter(_ :)时,先前的状态将传递到didEnter(from :)到当前状态。 如果这是状态机的第一个状态,则没有先前的状态,因此previousState将为nil。 在这种情况下,您可以在allKanjiListCoordinator上调用start()。 但是如果存在先前的状态,则意味着您应该pop到根视图控制器以返回到All屏幕。
构建并运行应用程序。 在List screen或Detail screen上,All按钮应该会返回到All屏幕。
都完成了,您重构了Kanji列表以使用GKStateMachine来管理应用中的导航。 做得好!
Apple关于GKState和GKStateMachine的文档非常宝贵。 您可能也有兴趣了解有关state machines的更多信息。
后记
本篇主要讲述了GameplayKit的实用状态机,感兴趣的给个赞或者关注~~~
