Skip to content

Commit 67b9d35

Browse files
committed
Merge pull request #36 from ReactKit/refactor
Refactor code for better typing, naming, and RouteMapping support.
2 parents 9abcb72 + 83af536 commit 67b9d35

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+4993
-3469
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ DerivedData
1717
*.xcuserstate
1818

1919
Carthage/Build
20+
.build
21+
Packages/

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "Carthage/Checkouts/xcconfigs"]
2+
path = Carthage/Checkouts/xcconfigs
3+
url = https://github.com/mrackwitz/xcconfigs.git

Cartfile.private

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github "mrackwitz/xcconfigs"

Cartfile.resolved

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github "mrackwitz/xcconfigs" "3.0"

Carthage/Checkouts/xcconfigs

Submodule xcconfigs added at 6b2682f

Configurations/Base.xcconfig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// Base.xcconfig
3+
// SwiftState
4+
//
5+
// Created by Yasuhiro Inami on 2015-12-09.
6+
// Copyright © 2015 Yasuhiro Inami. All rights reserved.
7+
//
8+
9+
CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer;
10+
MACOSX_DEPLOYMENT_TARGET = 10.9;
11+
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
12+
WATCHOS_DEPLOYMENT_TARGET = 2.0;
13+
TVOS_DEPLOYMENT_TARGET = 9.0;

Configurations/Debug.xcconfig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//
2+
// Debug.xcconfig
3+
// SwiftState
4+
//
5+
// Created by Yasuhiro Inami on 2015-12-09.
6+
// Copyright © 2015 Yasuhiro Inami. All rights reserved.
7+
//
8+
9+
#include "Base.xcconfig"
10+
11+
SWIFT_OPTIMIZATION_LEVEL = -Onone;

Configurations/Release.xcconfig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//
2+
// Release.xcconfig
3+
// SwiftState
4+
//
5+
// Created by Yasuhiro Inami on 2015-12-09.
6+
// Copyright © 2015 Yasuhiro Inami. All rights reserved.
7+
//
8+
9+
#include "Base.xcconfig"
10+
11+
SWIFT_OPTIMIZATION_LEVEL = -Owholemodule;

Package.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// Package.swift
3+
// SwiftState
4+
//
5+
// Created by Yasuhiro Inami on 2015-12-05.
6+
// Copyright © 2015 Yasuhiro Inami. All rights reserved.
7+
//
8+
9+
import PackageDescription
10+
11+
let package = Package(
12+
name: "SwiftState"
13+
)

README.md

Lines changed: 126 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,76 +11,78 @@ Elegant state machine for Swift.
1111
```swift
1212
enum MyState: StateType {
1313
case State0, State1, State2
14-
case AnyState // create case=Any
15-
16-
init(nilLiteral: Void) {
17-
self = AnyState
18-
}
1914
}
2015
```
2116

2217
```swift
23-
let machine = StateMachine<MyState, MyEvent>(state: .State0) { machine in
18+
// setup state machine
19+
let machine = Machine<MyState, NoEvent>(state: .State0) { machine in
2420

2521
machine.addRoute(.State0 => .State1)
26-
machine.addRoute(nil => .State2) { context in print("Any => 2, msg=\(context.userInfo)") }
27-
machine.addRoute(.State2 => nil) { context in print("2 => Any, msg=\(context.userInfo)") }
22+
machine.addRoute(.Any => .State2) { context in print("Any => 2, msg=\(context.userInfo)") }
23+
machine.addRoute(.State2 => .Any) { context in print("2 => Any, msg=\(context.userInfo)") }
2824

29-
// add handler (handlerContext = (event, transition, order, userInfo))
25+
// add handler (`context = (event, fromState, toState, userInfo)`)
3026
machine.addHandler(.State0 => .State1) { context in
3127
print("0 => 1")
3228
}
3329

3430
// add errorHandler
35-
machine.addErrorHandler { (event, transition, order, userInfo) in
31+
machine.addErrorHandler { event, fromState, toState, userInfo in
3632
print("[ERROR] \(transition.fromState) => \(transition.toState)")
3733
}
3834
}
3935

36+
// initial
37+
XCTAssertTrue(machine.state == .State0)
38+
4039
// tryState 0 => 1 => 2 => 1 => 0
40+
4141
machine <- .State1
42+
XCTAssertTrue(machine.state == .State1)
43+
4244
machine <- (.State2, "Hello")
45+
XCTAssertTrue(machine.state == .State2)
46+
4347
machine <- (.State1, "Bye")
48+
XCTAssertTrue(machine.state == .State1)
49+
4450
machine <- .State0 // fail: no 1 => 0
45-
46-
print("machine.state = \(machine.state)")
51+
XCTAssertTrue(machine.state == .State1)
4752
```
4853

4954
This will print:
5055

5156
```swift
5257
0 => 1
53-
Any => 2, msg=Hello
54-
2 => Any, msg=Bye
55-
[ERROR] 1 => 0
56-
machine.state = 1
58+
Any => 2, msg=Optional("Hello")
59+
2 => Any, msg=Optional("Bye")
60+
[ERROR] State1 => State0
5761
```
5862

5963
### Transition by Event
6064

61-
Use `<-!` operator to try transition by `Event` rather than specifying target `State` ([Test Case](https://github.com/ReactKit/SwiftState/blob/6858f8f49087c4b8b30bd980cfc81e8e74205718/SwiftStateTests/StateMachineEventTests.swift#L54-L76)).
65+
Use `<-!` operator to try transition by `Event` rather than specifying target `State`.
6266

6367
```swift
64-
enum MyEvent: StateEventType {
68+
enum MyEvent: EventType {
6569
case Event0, Event1
66-
case AnyEvent // create case=Any
67-
68-
init(nilLiteral: Void) {
69-
self = AnyEvent
70-
}
7170
}
7271
```
7372

7473
```swift
7574
let machine = StateMachine<MyState, MyEvent>(state: .State0) { machine in
7675

7776
// add 0 => 1 => 2
78-
machine.addRouteEvent(.Event0, transitions: [
77+
machine.addRoute(event: .Event0, transitions: [
7978
.State0 => .State1,
8079
.State1 => .State2,
8180
])
8281
}
83-
82+
83+
// initial
84+
XCTAssertEqual(machine.state, MyState.State0)
85+
8486
// tryEvent
8587
machine <-! .Event0
8688
XCTAssertEqual(machine.state, MyState.State1)
@@ -89,44 +91,121 @@ XCTAssertEqual(machine.state, MyState.State1)
8991
machine <-! .Event0
9092
XCTAssertEqual(machine.state, MyState.State2)
9193

94+
// tryEvent (fails)
95+
machine <-! .Event0
96+
XCTAssertEqual(machine.state, MyState.State2, "Event0 doesn't have 2 => Any")
97+
```
98+
99+
If there is no `Event`-based transition, use built-in `NoEvent` instead.
100+
101+
### State & Event enums with associated values
102+
103+
Above examples use _arrow-style routing_ which are easy to understand, but it lacks in ability to handle **state & event enums with associated values**. In such cases, use either of the following functions to apply _closure-style routing_:
104+
105+
- `machine.addRouteMapping(routeMapping)`
106+
- `RouteMapping`: `(event: E?, fromState: S, userInfo: Any?) -> S?`
107+
- `machine.addStateRouteMapping(stateRouteMapping)`
108+
- `StateRouteMapping`: `(fromState: S, userInfo: Any?) -> [S]?`
109+
110+
For example:
111+
112+
```swift
113+
enum StrState: StateType {
114+
case Str(String) ...
115+
}
116+
enum StrEvent: EventType {
117+
case Str(String) ...
118+
}
119+
120+
let machine = Machine<StrState, StrEvent>(state: .Str("initial")) { machine in
121+
122+
machine.addRouteMapping { event, fromState, userInfo -> StrState? in
123+
// no route for no-event
124+
guard let event = event else { return nil }
125+
126+
switch (event, fromState) {
127+
case (.Str("gogogo"), .Str("initial")):
128+
return .Str("Phase 1")
129+
case (.Str("gogogo"), .Str("Phase 1")):
130+
return .Str("Phase 2")
131+
case (.Str("finish"), .Str("Phase 2")):
132+
return .Str("end")
133+
default:
134+
return nil
135+
}
136+
}
137+
138+
}
139+
140+
// initial
141+
XCTAssertEqual(machine.state, StrState.Str("initial"))
142+
143+
// tryEvent (fails)
144+
machine <-! .Str("go?")
145+
XCTAssertEqual(machine.state, StrState.Str("initial"), "No change.")
146+
92147
// tryEvent
93-
let success = machine <-! .Event0
94-
XCTAssertEqual(machine.state, MyState.State2)
95-
XCTAssertFalse(success, "Event0 doesn't have 2 => Any")
148+
machine <-! .Str("gogogo")
149+
XCTAssertEqual(machine.state, StrState.Str("Phase 1"))
150+
151+
// tryEvent (fails)
152+
machine <-! .Str("finish")
153+
XCTAssertEqual(machine.state, StrState.Str("Phase 1"), "No change.")
154+
155+
// tryEvent
156+
machine <-! .Str("gogogo")
157+
XCTAssertEqual(machine.state, StrState.Str("Phase 2"))
158+
159+
// tryEvent (fails)
160+
machine <-! .Str("gogogo")
161+
XCTAssertEqual(machine.state, StrState.Str("Phase 2"), "No change.")
162+
163+
// tryEvent
164+
machine <-! .Str("finish")
165+
XCTAssertEqual(machine.state, StrState.Str("end"))
96166
```
97167

168+
This behaves very similar to JavaScript's safe state-container [rackt/Redux](https://github.com/rackt/redux), where `RouteMapping` can be interpretted as `Redux.Reducer`.
169+
98170
For more examples, please see XCTest cases.
99171

100172

101173
## Features
102174

103175
- Easy Swift syntax
104176
- Transition: `.State0 => .State1`, `[.State0, .State1] => .State2`
105-
- Try transition: `machine <- .State1`
106-
- Try transition + messaging: `machine <- (.State1, "GoGoGo")`
177+
- Try state: `machine <- .State1`
178+
- Try state + messaging: `machine <- (.State1, "GoGoGo")`
107179
- Try event: `machine <-! .Event1`
108180
- Highly flexible transition routing
109-
- using Condition
110-
- using AnyState (`nil` state)
111-
- or both (blacklisting): `nil => nil` + condition
112-
- Success/Error/Entry/Exit handlers with `order: UInt8` (no before/after handler stuff)
113-
- Removable routes and handlers
114-
- Chaining: `.State0 => .State1 => .State2`
115-
- Event: `machine.addRouteEvent("WakeUp", transitions); machine <-! "WakeUp"`
181+
- Using `Condition`
182+
- Using `.Any` state
183+
- Entry handling: `.Any => .SomeState`
184+
- Exit handling: `.SomeState => .Any`
185+
- Blacklisting: `.Any => .Any` + `Condition`
186+
- Using `.Any` event
187+
188+
- Route Mapping (closure-based routing): [#36](https://github.com/ReactKit/SwiftState/pull/36)
189+
- Success/Error handlers with `order: UInt8` (more flexible than before/after handlers)
190+
- Removable routes and handlers using `Disposable`
191+
- Route Chaining: `.State0 => .State1 => .State2`
116192
- Hierarchical State Machine: [#10](https://github.com/ReactKit/SwiftState/pull/10)
117193

118194
## Terms
119195

120-
Term | Class | Description
121-
--------- | ----------------------------- | ------------------------------------------
122-
State | `StateType` (protocol) | Mostly enum, describing each state e.g. `.State0`.
123-
Event | `StateEventType` (protocol) | Name for route-group. Transition can be fired via `Event` instead of explicitly targeting next `State`.
124-
Machine | `StateMachine` | State transition manager which can register `Route` and `Handler` separately for variety of transitions.
125-
Transition | `StateTransition` | `From-` and `to-` states represented as `.State1 => .State2`. If `nil` is used for either state, it will be represented as `.AnyState`.
126-
Route | `StateRoute` | `Transition` + `Condition`.
127-
Condition | `Transition -> Bool` | Closure for validating transition. If condition returns `false`, transition will fail and associated handlers will not be invoked.
128-
Handler | `HandlerContext -> Void` | Transition callback invoked after state has been changed.
129-
Chain | `StateTransitionChain` | Group of continuous routes represented as `.State1 => .State2 => .State3`
196+
Term | Type | Description
197+
------------- | ----------------------------- | ------------------------------------------
198+
State | `StateType` (protocol) | Mostly enum, describing each state e.g. `.State0`.
199+
Event | `EventType` (protocol) | Name for route-group. Transition can be fired via `Event` instead of explicitly targeting next `State`.
200+
State Machine | `Machine` | State transition manager which can register `Route`/`RouteMapping` and `Handler` separately for variety of transitions.
201+
Transition | `Transition` | `From-` and `to-` states represented as `.State1 => .State2`. Also, `.Any` can be used to represent _any state_.
202+
Route | `Route` | `Transition` + `Condition`.
203+
Condition | `Context -> Bool` | Closure for validating transition. If condition returns `false`, transition will fail and associated handlers will not be invoked.
204+
Route Mapping | `(event: E?, fromState: S, userInfo: Any?) -> S?` | Another way of defining routes **using closure instead of transition arrows (`=>`)**. This is useful when state & event are enum with associated values. Return value (`S?`) means preferred-`toState`, where passing `nil` means no routes available. See [#36](https://github.com/ReactKit/SwiftState/pull/36) for more info.
205+
State Route Mapping | `(fromState: S, userInfo: Any?) -> [S]?` | Another way of defining routes **using closure instead of transition arrows (`=>`)**. This is useful when state is enum with associated values. Return value (`[S]?`) means multiple `toState`s from single `fromState` (synonym for multiple routing e.g. `.State0 => [.State1, .State2]`). See [#36](https://github.com/ReactKit/SwiftState/pull/36) for more info.
206+
Handler | `Context -> Void` | Transition callback invoked when state has been changed successfully.
207+
Context | `(event: E?, fromState: S, toState: S, userInfo: Any?)` | Closure argument for `Condition` & `Handler`.
208+
Chain | `TransitionChain` / `RouteChain` | Group of continuous routes represented as `.State1 => .State2 => .State3`
130209

131210

132211
## Related Articles

Screenshots/logo.png

-15.1 KB
Loading

Sources/Disposable.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// Disposable.swift
3+
// ReactiveCocoa
4+
//
5+
// Created by Justin Spahr-Summers on 2014-06-02.
6+
// Copyright (c) 2014 GitHub. All rights reserved.
7+
//
8+
9+
//
10+
// NOTE:
11+
// This file is a partial copy from ReactiveCocoa v4.0.0-alpha.4 (removing `Atomic` dependency),
12+
// which has not been taken out as microframework yet.
13+
// https://github.com/ReactiveCocoa/ReactiveCocoa/issues/2579
14+
//
15+
// Note that `ActionDisposable` also works as `() -> ()` wrapper to help suppressing warning:
16+
// "Expression resolved to unused function", when returned function was not used.
17+
//
18+
19+
/// Represents something that can be “disposed,” usually associated with freeing
20+
/// resources or canceling work.
21+
public protocol Disposable {
22+
/// Whether this disposable has been disposed already.
23+
var disposed: Bool { get }
24+
25+
func dispose()
26+
}
27+
28+
/// A disposable that will run an action upon disposal.
29+
public final class ActionDisposable: Disposable {
30+
private var action: (() -> ())?
31+
32+
public var disposed: Bool {
33+
return action == nil
34+
}
35+
36+
/// Initializes the disposable to run the given action upon disposal.
37+
public init(action: () -> ()) {
38+
self.action = action
39+
}
40+
41+
public func dispose() {
42+
self.action?()
43+
self.action = nil
44+
}
45+
}

0 commit comments

Comments
 (0)