用于创建有限状态机的Swift微框架,旨在清晰性和可维护性。可以创建图
API文档在源中。有关示例代码和说明,请参见示例。更多介绍请参见Macoscope博客上的帖子。
SwiftyStateMachine是一个框架 - 您可以构建它并将其拖入到项目中。我们在发布页面上提供iOS和OS X的预构建框架的ZIP文件。
您还可以使用CocoaPods
# Podfile
pod 'SwiftyStateMachine', '0.3.0'
在这个示例中,我们将实现一个简单的状态机,它位于此文件的开始处
让我们先定义用于状态和事件的enum
enum Number {
case One, Two, Three
}
enum Operation {
case Increment, Decrement
}
接下来,我们必须指定状态机的布局。在SwiftyStateMachine中,这意味着创建一个模式。模式是不可变的struct
,可由多个StateMachine
实例使用。它们指示初始状态,并描述转换逻辑,即状态如何通过事件连接,以及状态转换期间执行的代码。
模式包含三种通用类型:"State
"和"Event
",我们上面定义过,以及代表与状态机关联对象的"Subject
"。为了简化我们的讨论,我们现在不会使用"Subject
",所以我们将它的类型指定为"Void
"。
import SwiftyStateMachine
let schema = StateMachineSchema<Number, Operation, Void>(initialState: .One) { (state, event) in
switch state {
case .One: switch event {
case .Decrement: return nil
case .Increment: return (.Two, { _ in print("1 → 2") })
// we used nil to ignore the event
// and _ to ignore the subject
}
case .Two: switch event {
case .Decrement: return (.One, { _ in print("2 → 1") })
case .Increment: return (.Three, { _ in print("2 → 3") })
}
case .Three: switch event {
case .Decrement: return (.Two, nil) // nil transition block
case .Increment: return nil
}
}
}
在定义了两个"enum
"之后,你可能期待有嵌套的"switch
"语句。:
为了理解上面的代码片段,查看初始化器的签名会有所帮助。
init(initialState: State,
transitionLogic: (State, Event) -> (State, (Subject -> ())?)?)
我们将转换逻辑指定为一个代码块。它接受两个参数:当前状态和处理的事件。它返回一个新状态和可选转换代码块的可选元组。当元组是"nil
"时,表示在给定的状态-事件对中没有转换,即在某些状态中应忽略某些事件。当元组非"nil
"时,它指定机器应转换到的新状态和转换后应调用的一段代码块。转换代码块是可选的。它将"Subject
"对象作为参数传递,在这个例子中,我们通过使用"_
"来忽略它。
现在,基于该模式创建一个机器并对其进行测试。
// we use () as subject because subject type is Void
var machine = StateMachine(schema: schema, subject: ())
machine.handleEvent(.Decrement) // nothing happens
if machine.state == .One { print("one") } // prints "one"
machine.handleEvent(.Increment) // prints "1 → 2"
if machine.state == .Two { print("two") } // prints "two"
酷!我们还可以通过提供一个"didTransitionCallback
"代码块来获取转换的通知。转换发生后调用该代码块,它带有三个参数:转换前的状态、引起转换的事件以及转换后的状态。
machine.didTransitionCallback = { (oldState, event, newState) in
print("changed state!")
}
关于图表的问题?SwiftyStateMachine可以用DOT 图形描述语言创建图表。为了创建图表,我们必须使用"GraphableStateMachineSchema
",它和常规的"StateMachineSchema
"有相同的初始化器,但它要求状态和事件类型实现DOTLabelable
协议。这个协议确保所有元素都有容易阅读的标签,并显示在图上(没有自动找到所有"enum
"情况的办法)。
extension Number: DOTLabelable {
static var DOTLabelableItems: [Number] {
return [.One, .Two, .Three]
}
// Implementing this property in not required but we show it here for the
// sake of completeness. You can use it to customize labels on the graph.
// In the following implementation we are basically returning `"\(self)"`.
// In fact, this protocol already has a default implementation that does
// just that. Because of this, we will skip `DOTLabel` when implementing
// `DOTLabelable` extension of `Operation`.
var DOTLabel: String {
switch self {
case .One: return "One"
case .Two: return "Two"
case .Three: return "Three"
}
}
}
extension Operation: DOTLabelable {
static var DOTLabelableItems: [Operation] {
return [.Increment, .Decrement]
}
}
当我们的类型实现"DOTLabelable
"时,我们可以像以前一样定义结构,但这次使用"GraphableStateMachineSchema
"。然后我们可以打印图表。
let schema = GraphableStateMachineSchema// ...
print(schema.DOTDigraph)
digraph {
graph [rankdir=LR]
0 [label="", shape=plaintext]
0 -> 1 [label="START"]
1 [label="One"]
2 [label="Two"]
3 [label="Three"]
1 -> 2 [label="Increment"]
2 -> 3 [label="Increment"]
2 -> 1 [label="Decrement"]
3 -> 2 [label="Decrement"]
}
在iOS上,我们甚至可以在每次在模拟器上运行应用时将图形文件保存在仓库中。
try schema.saveDOTDigraphIfRunningInSimulator(filepathRelativeToCurrentFile: "123.dot")
DOT文件可以用许多应用程序查看,包括免费的Graphviz。如果你使用Homebrew,你可以使用以下命令安装Graphviz:
brew update
brew install graphviz --with-app
brew linkapps graphviz
Graphviz带有一个"dot
"命令,可以用于在没有启动GUI应用程序的情况下生成图形图像。
dot -Tpng 123.dot > 123.png
这就是我们的例子和SwiftyStateMachine API之旅的结束。享受通过显式定义不同的状态及其之间的转换来改进你的代码吧!在这样做的时候,请记住以下两点:
1) 你的主题可能是引用类型(类)。通常将状态机作为主题的属性存储会创建一个循环引用,因此SwiftyStateMachine对基于类的主题使用弱引用。这意味着你必须在某处保留主题的强引用,但通常你已经这样做了。当主题引用变为"nil
"时,转换将不再执行。
2) 记住,Swift 的enum
可以有关联值——你可以在事件中传递额外的信息或在状态中存储数据。例如,如果你有一个带有抬头显示的游戏,你可以做类似这样的事:
enum HUDEvent {
case TakeDamage(Double)
// ...
}
// ...
machine.handleEvent(.TakeDamage(13.37))
如果您看到改进项目的方法,请留下评论,或者创建一个 问题 或发起一个 拉取请求。尽管如此,最好从创建问题开始,而不是直接发起拉取请求,因为我们可能会对建议的更改是否真正改进意见不一。
要运行测试,请安装 Carthage 并执行 carthage update
命令以下载和构建测试框架。
引入更改时,请尽量符合项目中的风格——无论是代码格式还是提交信息。我们建议遵循 GitHub Swift 风格指南,但有一点重要区别:使用 4 个空格而不是制表符。
谢谢!