Skip to content

Commit 53d8140

Browse files
committed
Merge pull request #10 from ReactKit/HierarchicalStateMachine
Add HierarchicalStateMachine.
2 parents 7468927 + b55720c commit 53d8140

File tree

3 files changed

+360
-0
lines changed

3 files changed

+360
-0
lines changed

SwiftState.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
1FA62038199660CA00460108 /* StateTransitionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA6202F199660CA00460108 /* StateTransitionTests.swift */; };
2929
1FB1EC8A199E515B00ABD937 /* MyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB1EC89199E515B00ABD937 /* MyEvent.swift */; };
3030
1FB1EC8F199E60F800ABD937 /* String+SwiftState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB1EC8E199E60F800ABD937 /* String+SwiftState.swift */; };
31+
1FD01B6019EC2B5C00DA1C91 /* HierarchicalStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD01B5F19EC2B5C00DA1C91 /* HierarchicalStateMachine.swift */; };
32+
1FD01B6119EC2B5C00DA1C91 /* HierarchicalStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD01B5F19EC2B5C00DA1C91 /* HierarchicalStateMachine.swift */; };
33+
1FD01B6319EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD01B6219EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift */; };
34+
1FD01B6419EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD01B6219EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift */; };
3135
1FF692041996625900E3CE40 /* SwiftState.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FA620001996601000460108 /* SwiftState.framework */; };
3236
4822F0A819D008E300F5F572 /* _TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA62027199660CA00460108 /* _TestCase.swift */; };
3337
4822F0A919D008E700F5F572 /* MyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA62029199660CA00460108 /* MyState.swift */; };
@@ -86,6 +90,8 @@
8690
1FA6202F199660CA00460108 /* StateTransitionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateTransitionTests.swift; sourceTree = "<group>"; };
8791
1FB1EC89199E515B00ABD937 /* MyEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyEvent.swift; sourceTree = "<group>"; };
8892
1FB1EC8E199E60F800ABD937 /* String+SwiftState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SwiftState.swift"; sourceTree = "<group>"; };
93+
1FD01B5F19EC2B5C00DA1C91 /* HierarchicalStateMachine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HierarchicalStateMachine.swift; sourceTree = "<group>"; };
94+
1FD01B6219EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HierarchicalStateMachineTests.swift; sourceTree = "<group>"; };
8995
4822F0A619D0085E00F5F572 /* SwiftStateTests-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SwiftStateTests-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
9096
4872D5AC19B4211900F326B5 /* SwiftState.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftState.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9197
/* End PBXFileReference section */
@@ -154,6 +160,7 @@
154160
1FA6201E1996606300460108 /* StateTransitionChain.swift */,
155161
1FA6201B1996606300460108 /* StateRoute.swift */,
156162
1FA6201C1996606300460108 /* StateRouteChain.swift */,
163+
1FD01B5F19EC2B5C00DA1C91 /* HierarchicalStateMachine.swift */,
157164
1FA620031996601000460108 /* Supporting Files */,
158165
);
159166
path = SwiftState;
@@ -180,6 +187,7 @@
180187
1FA6202F199660CA00460108 /* StateTransitionTests.swift */,
181188
1FA6202E199660CA00460108 /* StateTransitionChainTests.swift */,
182189
1FA6202D199660CA00460108 /* StateRouteTests.swift */,
190+
1FD01B6219EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift */,
183191
1FA6200D1996601000460108 /* Supporting Files */,
184192
);
185193
path = SwiftStateTests;
@@ -387,6 +395,7 @@
387395
1FA620201996606300460108 /* StateEventType.swift in Sources */,
388396
1FA620221996606300460108 /* StateRoute.swift in Sources */,
389397
1FA620211996606300460108 /* StateMachine.swift in Sources */,
398+
1FD01B6019EC2B5C00DA1C91 /* HierarchicalStateMachine.swift in Sources */,
390399
1FA620261996606300460108 /* StateType.swift in Sources */,
391400
1FA620231996606300460108 /* StateRouteChain.swift in Sources */,
392401
);
@@ -398,6 +407,7 @@
398407
files = (
399408
1FB1EC8A199E515B00ABD937 /* MyEvent.swift in Sources */,
400409
1F198C5C19972320001C3700 /* QiitaTests.swift in Sources */,
410+
1FD01B6319EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift in Sources */,
401411
1FA62038199660CA00460108 /* StateTransitionTests.swift in Sources */,
402412
1FA62033199660CA00460108 /* StateMachineChainTests.swift in Sources */,
403413
1FA62030199660CA00460108 /* _TestCase.swift in Sources */,
@@ -416,6 +426,7 @@
416426
files = (
417427
4822F0AC19D008EB00F5F572 /* QiitaTests.swift in Sources */,
418428
4822F0AB19D008EB00F5F572 /* BasicTests.swift in Sources */,
429+
1FD01B6419EC2B6700DA1C91 /* HierarchicalStateMachineTests.swift in Sources */,
419430
4822F0AF19D008EB00F5F572 /* StateMachineEventTests.swift in Sources */,
420431
4822F0A819D008E300F5F572 /* _TestCase.swift in Sources */,
421432
4822F0AA19D008E700F5F572 /* MyEvent.swift in Sources */,
@@ -438,6 +449,7 @@
438449
48797D5F19B42CCE0085D80F /* StateTransition.swift in Sources */,
439450
48797D6319B42CD40085D80F /* StateType.swift in Sources */,
440451
48797D5E19B42CCE0085D80F /* StateMachine.swift in Sources */,
452+
1FD01B6119EC2B5C00DA1C91 /* HierarchicalStateMachine.swift in Sources */,
441453
48797D6119B42CCE0085D80F /* StateRoute.swift in Sources */,
442454
48797D6419B42CD40085D80F /* StateEventType.swift in Sources */,
443455
);
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//
2+
// HierarchicalStateMachine.swift
3+
// SwiftState
4+
//
5+
// Created by Yasuhiro Inami on 2014/10/13.
6+
// Copyright (c) 2014年 Yasuhiro Inami. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public typealias HSM = HierarchicalStateMachine<String, String>
12+
13+
//
14+
// NOTE:
15+
// When subclassing StateMachine<StateType, StateEventType>,
16+
// don't directly set String as a replacement of StateType (generic type)
17+
// due to Xcode6.1-GM2 generics bug(?) causing EXC_I386_GPFLT when overriding method e.g. `hasRoute()`.
18+
//
19+
// Ideally, class `HierarchicalStateMachine` should be declared as following:
20+
//
21+
// `public class HierarchicalStateMachine: StateMachine<String, String>`
22+
//
23+
// To avoid above issue, we use `typealias HSM` instead.
24+
//
25+
26+
/// nestable StateMachine with StateType as String
27+
public class HierarchicalStateMachine<S: StateType, E: StateEventType>: StateMachine<S, E>, Printable
28+
{
29+
private var _submachines = [String : HSM]()
30+
31+
public let name: String
32+
33+
/// init with submachines
34+
public init(name: String, submachines: [HSM]? = nil, state: State, initClosure: (StateMachine<State, Event> -> Void)? = nil)
35+
{
36+
self.name = name
37+
38+
if let submachines = submachines {
39+
for submachine in submachines {
40+
self._submachines[submachine.name] = submachine
41+
}
42+
}
43+
44+
super.init(state: state, initClosure: initClosure)
45+
}
46+
47+
public var description: String
48+
{
49+
return self.name
50+
}
51+
52+
///
53+
/// Converts dot-chained state sent from mainMachine into (submachine, substate) tuple.
54+
/// e.g.
55+
///
56+
/// - state="MainState1" will return (nil, "MainState1")
57+
/// - state="SubMachine1.State1" will return (submachine1, "State1")
58+
/// - state="" (nil) will return (nil, nil)
59+
///
60+
private func _submachineTupleForState(state: State) -> (HSM?, HSM.State)
61+
{
62+
assert(state is HSM.State, "HSM state must be String.")
63+
64+
let components = split(state as HSM.State, { $0 == "." }, maxSplit: 1)
65+
66+
switch components.count {
67+
case 2:
68+
let submachineName = components[0]
69+
let substate = components[1]
70+
return (self._submachines[submachineName], substate)
71+
72+
case 1:
73+
let state = components[0]
74+
return (nil, state)
75+
76+
default:
77+
// NOTE: reaches here when state="" (empty) as AnyState
78+
return (nil, nil)
79+
}
80+
}
81+
82+
public override var state: State
83+
{
84+
// NOTE: returning `substate` is not reliable (as a spec), so replace it with actual `submachine.state` instead
85+
let (submachine, substate) = self._submachineTupleForState(self._state)
86+
87+
if let submachine = submachine {
88+
self._state = "\(submachine.name).\(submachine.state)" as State
89+
}
90+
91+
return self._state
92+
}
93+
94+
public override func hasRoute(transition: Transition, forEvent event: Event = nil) -> Bool
95+
{
96+
let (fromSubmachine, fromSubstate) = self._submachineTupleForState(transition.fromState)
97+
let (toSubmachine, toSubstate) = self._submachineTupleForState(transition.toState)
98+
99+
// check submachine-internal routes
100+
if fromSubmachine != nil && toSubmachine != nil && fromSubmachine === toSubmachine {
101+
return fromSubmachine!.hasRoute(fromSubstate => toSubstate, forEvent: nil)
102+
}
103+
104+
return super.hasRoute(transition, forEvent: event)
105+
}
106+
107+
internal override func _addRoute(var route: Route, forEvent event: Event = nil) -> RouteID
108+
{
109+
let originalCondition = route.condition
110+
111+
let condition: Condition = { transition -> Bool in
112+
113+
let (fromSubmachine, fromSubstate) = self._submachineTupleForState(route.transition.fromState)
114+
let (toSubmachine, toSubstate) = self._submachineTupleForState(route.transition.toState)
115+
116+
//
117+
// For external-route, don't let mainMachine switch to submachine.state=toSubstate
118+
// when current submachine.state is not toSubstate.
119+
//
120+
// e.g. ignore `"MainState0" => "Sub1.State1"` transition when
121+
// `mainMachine.state="MainState0"` but `submachine.state="State2"` (not "State1")
122+
//
123+
if toSubmachine != nil && toSubmachine!.state != toSubstate && fromSubmachine !== toSubmachine {
124+
return false
125+
}
126+
127+
return originalCondition?(transition: transition) ?? true
128+
}
129+
130+
route = Route(transition: route.transition, condition: condition)
131+
132+
return super._addRoute(route, forEvent: event)
133+
}
134+
135+
// TODO: apply mainMachine's events to submachines
136+
internal override func _tryState(state: State, userInfo: Any? = nil, forEvent event: Event) -> Bool
137+
{
138+
assert(state is HSM.State, "HSM state must be String.")
139+
140+
let fromState = self.state
141+
let toState = state
142+
let transition = fromState => toState
143+
144+
let (fromSubmachine, fromSubstate) = self._submachineTupleForState(fromState)
145+
let (toSubmachine, toSubstate) = self._submachineTupleForState(toState)
146+
147+
// try changing submachine-internal state
148+
if fromSubmachine != nil && toSubmachine != nil && fromSubmachine === toSubmachine {
149+
150+
if toSubmachine!.canTryState(toSubstate, forEvent: event as HSM.Event) {
151+
152+
//
153+
// NOTE:
154+
// Change mainMachine's state first to invoke its handlers
155+
// before changing toSubmachine's state because mainMachine.state relies on it.
156+
//
157+
super._tryState(toState, userInfo: userInfo, forEvent: event)
158+
159+
toSubmachine! <- (toSubstate as HSM.State)
160+
161+
return true
162+
}
163+
164+
}
165+
166+
return super._tryState(toState, userInfo: userInfo, forEvent: event)
167+
}
168+
}

0 commit comments

Comments
 (0)