版本记录
版本号 | 时间 |
---|---|
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的实用状态机,感兴趣的给个赞或者关注~~~