测试测试 | ✓ |
Lang语言 | Obj-CObjective C |
许可 | MIT |
发布最新发布 | 2017年10月 |
SwiftSwift 版本 | 3.2 |
由 Shaheen Ghiassy 和 Daniel Beard 维护。
StateKit 是一个用于 iOS、OSX、tvOS 或 watchOS 的框架,用于捕获、记录和管理状态,以便保持您的应用程序代码冷静和理智。
让我们看看使用 StateKit 来管理加载状态和常规状态的快速、小型示例。
我们创建状态图如下
NSDictionary *chart = @{@"root":@{
@"enterState":^(SKStateChart *sc) {
[sc goToState:@"loading"];
},
SKDefineTransition_Event2State(@"apiRespondedSuccessfully", @"regularView"),
@"subStates":@{
@"regularView":@{
@"enterState":^(SKStateChart *sc) {
// setup the regular view
}},
@"loading":@{
@"enterState":^(SKStateChart *sc) {
// fetch data from the api
// show the loading spinner
},
@"exitState":^(SKStateChart *sc) {
// remove loading spinner
}}}}};
SKStateChart *stateChart = [[SKStateChart alloc] initWithStateChart:chart];
或者,在 Swift 中
let chart: Dictionary<String,AnyObject> = [
"root" : [
"enterState" : SKStateBlockify({(sc: SKStateChart) -> Void in
sc.goToState("loading")
}),
"apiRespondedSuccessfully" : SKStateBlockify({(sc: SKStateChart) -> Void in
sc.goToState("regularView")
}),
"subStates" : [
"regularView" : [
"enterState" : SKStateBlockify({(sc: SKStateChart) -> Void in
// setup the regular view
})
],
"loading" : [
"enterState" : SKStateBlockify({(sc: SKStateChart) -> Void in
// fetch data from the api
// show the loading spinner
}),
"exitState" : SKStateBlockify({(sc: SKStateChart) -> Void in
// remove loading spinner
}),
]
]
]
]
let stateChart: SKStateChart = SKStateChart(stateChart: chart)
注意:如果您需要集成 Swift,您需要在 Podfile 中添加 use_frameworks!,并将您的 Pod 路径修改为 pod "StateKit/Swift",以便访问辅助 typealias(类型别名)/ 函数。
上述字典被解释为一个类似这样的树形数据结构
让我们跟随这个特定的状态图发生的故事
在 init
时,状态图始终以 root
作为当前状态开始。当我们进入 root
状态时,状态图看到 root
状态中有一个 enterState
块,因此运行相关的块。该块将状态图导向 loading
状态。因此,状态图继续遍历树到 loading
状态。当我们进入 loading
状态时,运行 loading
状态上的 enterState
块。在这里,我们可以让应用程序从 API 获取数据并将指示器渲染到视图中。注意,状态图正在指导应用程序做什么。
当 API 成功响应时,我们向状态图发送适当的消息 [stateChart sendMessage:@"apiRespondedSuccessfully"]
。从应用程序代码的角度来看,我们无需做任何其他事情,我们假设状态图将在必要时指导任何后续步骤。
当状态图收到消息 apiRespondedSuccessfully
时,状态图会查找适当的消息处理程序并运行消息处理程序的块。在这个例子中,块将状态图导向 regularView
状态。
当状态图从 loading
状态遍历到 regularView
状态时,我们可以以高精度处理 alloc/dealloc 对象。当我们退出 loading
状态时,我们清理加载 UI(例如移除/dealloc 指示器等),当我们进入 regularView
状态时,我们设置适当的 UI。
这是一个基本的状态图示例,但它展示了应用程序流程可以通过状态图智能地管理和控制。
他们说可以通过开发人员对应用程序流程控制的掌握程度来衡量他们的能力。流程控制可以通过多种方式处理,但尤其是在前端应用程序中,准确捕获和操作状态来管理应用程序的流程控制是必不可少的。
StateKit赋予您以易于阅读和维护的方式轻松创建复杂流程控制的能力。
创建状态图的语法如下
所有状态图都必须有一个根状态 - 否则StateKit会抛出异常。以下是最小可行状态图。
NSDictionary *chart = @{@"root":@{}};
SKStateChart *stateChart = [[SKStateChart alloc] initWithStateChart:chart];
消息在状态字典条目的顶层定义。要将消息apiRespondedSuccess
和apiRespondedError
添加到根状态,我们会这样做
NSDictionary *chart = @{@"root":@{
@"apiRespondedSuccess":^(SKStateChart *sc) {
// show the page
},
@"apiRespondedError":^(SKStateChart *sc) {
// show the error page
}}};
当根状态收到这两个消息之一时,将运行相关的块。
注意:消息的键/值对必须是字符串/块的类型。
注意,状态图的引用作为sc
传递给函数。虽然您可能会从字典之外的位置引用状态图实例,但仍建议使用传递的引用。
子状态在关键字subStates
下定义为字典。如果我们想在上面示例中添加一个loading
子状态,我们会写
NSDictionary *chart = @{@"root":@{
@"apiRespondedSuccess":^(SKStateChart *sc) {
// show the page
},
@"apiRespondedError":^(SKStateChart *sc) {
// show the error page
},
@"subStates":@{
@"loading":@{
// put the loading's state messages and substates here
}}}};
注意:子状态的键/值对必须是字符串/字典的类型。
事件是状态图的核心。创建状态图后,外部世界可以开始向状态图发送消息,以了解正在发生的事情。状态图将解释消息并运行相应的接收器块(如果有)。
[stateChart sendMessage:@"userPressedTheRedButton"]
消息首先发送到当前状态,以查看是否存在接收该消息的接收器。如果当前状态对消息没有响应,状态图将开始向上冒泡到树中,寻找任何响应消息的父状态。如果当前状态加上任何当前状态的父状态都没有对消息做出响应,则消息将被静默忽略。
以下是一个虽然编造但有用的示例,说明在各种情况下消息冒泡是如何工作的。假设以下状态图
NSDictionary *chart = @{@"root":@{
@"subStates":@{
@"a":@{
@"subStates":@{
@"d":@{
@"userPressedButton":^(SKStateChart *sc) {
NSLog(@"state d says hi");
},
@"subStates":@{
@"j":@{}
}
}
}
},
@"b":@{
@"userPressedButton":^(SKStateChart *sc) {
NSLog(@"state b says hi");
},
@"subStates":@{
@"e":@{},
@"f":@{
@"subStates":@{
@"g":@{
@"userPressedButton":^(SKStateChart *sc) {
NSLog(@"state g says hi");
},
@"subStates":@{
@"h":@{},
@"i":@{}
}}}}}},
@"c":@{
@"subStates":@{
@"k":@{}
}}}}};
这将转换为以下树
将消息userPressedButton
发送到起始图,其意义将取决于状态图当前所在的状态。
以下表格显示了发送消息userPressedButton
到状态图并给定各种起始当前状态时的输出。
当前状态 | 发送userPressedButton 会输出 |
---|---|
root | 无 |
A | 无 |
B | state b says hi |
C | 无 |
D | state d says hi |
E | state b says hi |
F | state b says hi |
G | state g says hi |
H | state g says hi |
I | state g says hi |
J | state d says hi |
K | 无 |
状态遍历是状态图的另一个重要方面。当从一个状态转换到另一个状态时,状态图不会直接从一个状态转换到另一个状态。相反,状态图遍历树来从一个状态转换到另一个状态。这种树遍历与状态事件的结合是一种非常强大的组合,可以精确管理应用程序的流程控制。
从一个状态过渡到另一个状态的逻辑采取以下步骤
exitState
块(如果存在)。enterState
块。在图形上,这看起来像
当状态图遍历状态时,它将检查接触到的每个状态,如果存在,则在该状态下运行事件块。向你的状态图中添加事件块不是必需的。
当状态图进入状态时,它会运行如果存在的enterState
块。而且当状态图遍历树时,它会运行如果存在的exitState
块。
在上面的示例中,将运行状态G
和D
的exitState
块。将运行状态E
和H
的enterState
块。
请注意,对状态B
没有运行任何操作,因为我们没有进入或退出该状态。
许多初次了解状态图的开发者自然倾向于告知状态图要移动到哪个状态。不要这样做。外部世界通过发送消息来告诉状态图发生了什么,而状态图的职责是解释这些消息并根据需要(如果有的话)相应地操纵状态。
不要
[statechart goToState:"red"];
做
[stateChart sendMessage:@"userPressedTheRedButton"]
然后在你的状态图中这样做
@"userPressedTheRedButton":^(SKStateChart *sc) {
[sc goToState:@"red"];
}
或者甚至更简单
SKDefineTransition_Event2State(@"userPressedTheRedButton", @"red")
向状态图发送消息是其关键操作之一。不要害怕发送大量消息。并且不要害怕发送当前无效的消息 -很多时候这是良好的编程实践。
例如,想想看一个计时器。每一分钟,它在整分钟时向状态图发送消息 minuteUpdated
。如果状态图处于它关心的接收消息 minuteUpdated
的状态,它可以选择采取行动。如果状态图处于它不关心的 minuteUpdated
消息的状态,那么什么都不会发生 —— 这是一件好事。
状态图就像交响乐团的指挥。它知道所有的演奏者,并告诉他们何时演奏乐器。但不会代他们演奏乐器。同样,状态图应该调用函数,但详细逻辑应该不包含在状态图中。
做
@"enterState":(SKStateChart *sc) {
[view setupUI];
}
不要
@"enterState":(SKStateChart *sc) {
UILabel *label = [[UILabel alloc] init];
label = [[UILabel alloc] init];
label.text = @"StateKit!!!";
label.font = [UIFont systemFontOfSize:26];
label.textColor = [UIColor blueColor];
[self.view addSubview:label];
// ... more view logic
}
当到达新状态时,状态图从根状态开始,执行广度优先搜索,直到找到目标状态。虽然状态名称不会被强制要求是唯一的,但 bfs 搜索的结果可能会产生意外的结果。因此,最好避免重复的状态名称。
当在状态间转换时,状态图会在遍历树的期间调用事件块。小心在这些块中放置 goToState
指令,因为可能导致状态图在到达最终目标之前经历一段意外的旅程。
但正如提供的示例所示,有时在 enterState
块中放置 goToState
指令是合适的 —— 只需小心。
为了避免保留周期,在状态图中使用 weak-self 引用。
做
__weak UIViewController *weakSelf = self;
NSDictionary *chart = @{@"root":@{
@"enterState":^(SKStateChart *sc) {
weakSelf.title = @"hi";
}}};
let chart: Dictionary<String,AnyObject> = [
"root" : [
"enterState" : SKStateBlockify({ [weak self] (sc: SKStateChart) -> Void in
self?.title = "hi"
}),
]
]
不要
NSDictionary *chart = @{@"root":@{
@"enterState":^(SKStateChart *sc) {
self.title = @"hi";
}}};
let chart: Dictionary<String,AnyObject> = [
"root" : [
"enterState" : SKStateBlockify({(sc: SKStateChart) -> Void in
self.title = "hi"
}),
]
]
有可能创建一个有效的状态图,使其能够无限期地转换状态。这显然是错误的 - 不要这样做。
为了辅助,StateKit 如果在一次操作中有超过100个状态转换将抛出异常
StateKit 是一种有限状态机,但不是在传统意义上的 FSM。
FSMs 以图的形式管理状态 - StateKit 以树的形式管理状态。虽然所有树都是图,但并非所有图都是树。这个区别虽然微乎其微,但非常重要。
此外,StateKit 在基本数据结构中增加了事件和树遍历逻辑,从而允许快速应用程序开发。
随着经验的积累,你将能够识别哪些问题更适合用状态图来表示,而不是用有限状态机。
如果您正在寻找适用于 iOS 或 OS X 开发的经典 有限状态机,请查看
StateKit 完全测试并通过。
要运行单元测试,请打开文件 ./Example/StateKit.xcworkspace
。然后按 command-U
运行测试。
如前所述,如果您想包含 Swift 支持,并获得在闭包和 Objective-C 块之间桥梁的辅助函数,请修改 Podfile 的 StateKit 引用为:
pod "StateKit/Swift"
将 StateKit 导入必要的类
#import <SKStateChart.h>
并创建/实例化您的事件图表
NSDictionary *chart = @{@"root":@{}};
SKStateChart *stateChart = [[SKStateChart alloc] initWithStateChart:chart];
Shaheen Ghiassy, [email protected]
StateKit 在 BSD 许可协议下可用。有关更多信息,请参阅 LICENSE 文件。