Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor code for better typing, naming, and RouteMapping support. #36

Merged
merged 28 commits into from
Dec 10, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cc6bc8f
[Test] Add FrogcjnTest (event + associated value).
inamiy Nov 1, 2015
ef07416
Refactor code for better typing, naming, and routeMapping support.
inamiy Nov 14, 2015
465146a
[Test] Remove unnecessary `case Any`.
inamiy Nov 14, 2015
da9b9b4
Remove unnecessary methods.
inamiy Nov 24, 2015
c9f6db9
Add comment.
inamiy Nov 24, 2015
32630e2
Rename `Mapping` to `RouteMapping`.
inamiy Nov 24, 2015
93f184b
[Test] Organize tests.
inamiy Nov 24, 2015
bfd9ed2
Remove verbose methods in `Transition` & `Route`.
inamiy Nov 24, 2015
439d5a8
Organize code.
inamiy Nov 28, 2015
12cb3b8
[Test] Add testHasRoute_anyEvent()
inamiy Nov 28, 2015
2eeff1f
Add `final` modifiers.
inamiy Nov 28, 2015
1be6782
[Test] Re-add HierarchicalMachineTests which was deleted in ef07416.
inamiy Nov 29, 2015
8e093da
Update README.md & BasicTests.
inamiy Nov 30, 2015
2986012
Resize logo.png
inamiy Nov 30, 2015
f7ce748
Refactor code by separating event-only-driven `Machine` and `StateMac…
inamiy Dec 5, 2015
8ade68e
[Test] Add more RouteMapping tests.
inamiy Dec 5, 2015
e9c1bf3
[Test] Improve 8ade68e & update README.md
inamiy Dec 5, 2015
c1c18ec
Add `addStateRouteMapping()` & rename `EventRouteMapping` to `RouteMa…
inamiy Dec 7, 2015
b42ac6d
Update README.md
inamiy Dec 7, 2015
4c15f86
Conform State<S> & Event<E> to RawRepresentable.
inamiy Dec 8, 2015
edc0f3e
Set codeCoverageEnabled=YES.
inamiy Dec 8, 2015
2c9844f
[Test] Add StateTests & EventTests.
inamiy Dec 8, 2015
ce2ef2a
Fix RouteMapping + handler.
inamiy Dec 8, 2015
56b8c76
Simplify `machine.addRoutes()`.
inamiy Dec 8, 2015
e49b1c6
[Test] Improve code coverage.
inamiy Dec 8, 2015
c1751bf
Create universal framework to support watchOS & tvOS by using xcconfigs.
inamiy Dec 8, 2015
c6b59e4
Merge pull request #38 from ReactKit/universal-framework
inamiy Dec 8, 2015
83af536
Remove unnecessary xcodeproj settings.
inamiy Dec 9, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ DerivedData
*.xcuserstate

Carthage/Build
.build
Packages/
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "Carthage/Checkouts/xcconfigs"]
path = Carthage/Checkouts/xcconfigs
url = https://github.com/mrackwitz/xcconfigs.git
1 change: 1 addition & 0 deletions Cartfile.private
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github "mrackwitz/xcconfigs"
1 change: 1 addition & 0 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github "mrackwitz/xcconfigs" "3.0"
1 change: 1 addition & 0 deletions Carthage/Checkouts/xcconfigs
Submodule xcconfigs added at 6b2682
13 changes: 13 additions & 0 deletions Configurations/Base.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Base.xcconfig
// SwiftState
//
// Created by Yasuhiro Inami on 2015-12-09.
// Copyright © 2015 Yasuhiro Inami. All rights reserved.
//

CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer;
MACOSX_DEPLOYMENT_TARGET = 10.9;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
WATCHOS_DEPLOYMENT_TARGET = 2.0;
TVOS_DEPLOYMENT_TARGET = 9.0;
11 changes: 11 additions & 0 deletions Configurations/Debug.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// Debug.xcconfig
// SwiftState
//
// Created by Yasuhiro Inami on 2015-12-09.
// Copyright © 2015 Yasuhiro Inami. All rights reserved.
//

#include "Base.xcconfig"

SWIFT_OPTIMIZATION_LEVEL = -Onone;
11 changes: 11 additions & 0 deletions Configurations/Release.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// Release.xcconfig
// SwiftState
//
// Created by Yasuhiro Inami on 2015-12-09.
// Copyright © 2015 Yasuhiro Inami. All rights reserved.
//

#include "Base.xcconfig"

SWIFT_OPTIMIZATION_LEVEL = -Owholemodule;
13 changes: 13 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Package.swift
// SwiftState
//
// Created by Yasuhiro Inami on 2015-12-05.
// Copyright © 2015 Yasuhiro Inami. All rights reserved.
//

import PackageDescription

let package = Package(
name: "SwiftState"
)
173 changes: 126 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,76 +11,78 @@ Elegant state machine for Swift.
```swift
enum MyState: StateType {
case State0, State1, State2
case AnyState // create case=Any

init(nilLiteral: Void) {
self = AnyState
}
}
```

```swift
let machine = StateMachine<MyState, MyEvent>(state: .State0) { machine in
// setup state machine
let machine = Machine<MyState, NoEvent>(state: .State0) { machine in

machine.addRoute(.State0 => .State1)
machine.addRoute(nil => .State2) { context in print("Any => 2, msg=\(context.userInfo)") }
machine.addRoute(.State2 => nil) { context in print("2 => Any, msg=\(context.userInfo)") }
machine.addRoute(.Any => .State2) { context in print("Any => 2, msg=\(context.userInfo)") }
machine.addRoute(.State2 => .Any) { context in print("2 => Any, msg=\(context.userInfo)") }

// add handler (handlerContext = (event, transition, order, userInfo))
// add handler (`context = (event, fromState, toState, userInfo)`)
machine.addHandler(.State0 => .State1) { context in
print("0 => 1")
}

// add errorHandler
machine.addErrorHandler { (event, transition, order, userInfo) in
machine.addErrorHandler { event, fromState, toState, userInfo in
print("[ERROR] \(transition.fromState) => \(transition.toState)")
}
}

// initial
XCTAssertTrue(machine.state == .State0)

// tryState 0 => 1 => 2 => 1 => 0

machine <- .State1
XCTAssertTrue(machine.state == .State1)

machine <- (.State2, "Hello")
XCTAssertTrue(machine.state == .State2)

machine <- (.State1, "Bye")
XCTAssertTrue(machine.state == .State1)

machine <- .State0 // fail: no 1 => 0

print("machine.state = \(machine.state)")
XCTAssertTrue(machine.state == .State1)
```

This will print:

```swift
0 => 1
Any => 2, msg=Hello
2 => Any, msg=Bye
[ERROR] 1 => 0
machine.state = 1
Any => 2, msg=Optional("Hello")
2 => Any, msg=Optional("Bye")
[ERROR] State1 => State0
```

### Transition by Event

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)).
Use `<-!` operator to try transition by `Event` rather than specifying target `State`.

```swift
enum MyEvent: StateEventType {
enum MyEvent: EventType {
case Event0, Event1
case AnyEvent // create case=Any

init(nilLiteral: Void) {
self = AnyEvent
}
}
```

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

// add 0 => 1 => 2
machine.addRouteEvent(.Event0, transitions: [
machine.addRoute(event: .Event0, transitions: [
.State0 => .State1,
.State1 => .State2,
])
}


// initial
XCTAssertEqual(machine.state, MyState.State0)

// tryEvent
machine <-! .Event0
XCTAssertEqual(machine.state, MyState.State1)
Expand All @@ -89,44 +91,121 @@ XCTAssertEqual(machine.state, MyState.State1)
machine <-! .Event0
XCTAssertEqual(machine.state, MyState.State2)

// tryEvent (fails)
machine <-! .Event0
XCTAssertEqual(machine.state, MyState.State2, "Event0 doesn't have 2 => Any")
```

If there is no `Event`-based transition, use built-in `NoEvent` instead.

### State & Event enums with associated values

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_:

- `machine.addRouteMapping(routeMapping)`
- `RouteMapping`: `(event: E?, fromState: S, userInfo: Any?) -> S?`
- `machine.addStateRouteMapping(stateRouteMapping)`
- `StateRouteMapping`: `(fromState: S, userInfo: Any?) -> [S]?`

For example:

```swift
enum StrState: StateType {
case Str(String) ...
}
enum StrEvent: EventType {
case Str(String) ...
}

let machine = Machine<StrState, StrEvent>(state: .Str("initial")) { machine in

machine.addRouteMapping { event, fromState, userInfo -> StrState? in
// no route for no-event
guard let event = event else { return nil }

switch (event, fromState) {
case (.Str("gogogo"), .Str("initial")):
return .Str("Phase 1")
case (.Str("gogogo"), .Str("Phase 1")):
return .Str("Phase 2")
case (.Str("finish"), .Str("Phase 2")):
return .Str("end")
default:
return nil
}
}

}

// initial
XCTAssertEqual(machine.state, StrState.Str("initial"))

// tryEvent (fails)
machine <-! .Str("go?")
XCTAssertEqual(machine.state, StrState.Str("initial"), "No change.")

// tryEvent
let success = machine <-! .Event0
XCTAssertEqual(machine.state, MyState.State2)
XCTAssertFalse(success, "Event0 doesn't have 2 => Any")
machine <-! .Str("gogogo")
XCTAssertEqual(machine.state, StrState.Str("Phase 1"))

// tryEvent (fails)
machine <-! .Str("finish")
XCTAssertEqual(machine.state, StrState.Str("Phase 1"), "No change.")

// tryEvent
machine <-! .Str("gogogo")
XCTAssertEqual(machine.state, StrState.Str("Phase 2"))

// tryEvent (fails)
machine <-! .Str("gogogo")
XCTAssertEqual(machine.state, StrState.Str("Phase 2"), "No change.")

// tryEvent
machine <-! .Str("finish")
XCTAssertEqual(machine.state, StrState.Str("end"))
```

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`.

For more examples, please see XCTest cases.


## Features

- Easy Swift syntax
- Transition: `.State0 => .State1`, `[.State0, .State1] => .State2`
- Try transition: `machine <- .State1`
- Try transition + messaging: `machine <- (.State1, "GoGoGo")`
- Try state: `machine <- .State1`
- Try state + messaging: `machine <- (.State1, "GoGoGo")`
- Try event: `machine <-! .Event1`
- Highly flexible transition routing
- using Condition
- using AnyState (`nil` state)
- or both (blacklisting): `nil => nil` + condition
- Success/Error/Entry/Exit handlers with `order: UInt8` (no before/after handler stuff)
- Removable routes and handlers
- Chaining: `.State0 => .State1 => .State2`
- Event: `machine.addRouteEvent("WakeUp", transitions); machine <-! "WakeUp"`
- Using `Condition`
- Using `.Any` state
- Entry handling: `.Any => .SomeState`
- Exit handling: `.SomeState => .Any`
- Blacklisting: `.Any => .Any` + `Condition`
- Using `.Any` event

- Route Mapping (closure-based routing): [#36](https://github.com/ReactKit/SwiftState/pull/36)
- Success/Error handlers with `order: UInt8` (more flexible than before/after handlers)
- Removable routes and handlers using `Disposable`
- Route Chaining: `.State0 => .State1 => .State2`
- Hierarchical State Machine: [#10](https://github.com/ReactKit/SwiftState/pull/10)

## Terms

Term | Class | Description
--------- | ----------------------------- | ------------------------------------------
State | `StateType` (protocol) | Mostly enum, describing each state e.g. `.State0`.
Event | `StateEventType` (protocol) | Name for route-group. Transition can be fired via `Event` instead of explicitly targeting next `State`.
Machine | `StateMachine` | State transition manager which can register `Route` and `Handler` separately for variety of transitions.
Transition | `StateTransition` | `From-` and `to-` states represented as `.State1 => .State2`. If `nil` is used for either state, it will be represented as `.AnyState`.
Route | `StateRoute` | `Transition` + `Condition`.
Condition | `Transition -> Bool` | Closure for validating transition. If condition returns `false`, transition will fail and associated handlers will not be invoked.
Handler | `HandlerContext -> Void` | Transition callback invoked after state has been changed.
Chain | `StateTransitionChain` | Group of continuous routes represented as `.State1 => .State2 => .State3`
Term | Type | Description
------------- | ----------------------------- | ------------------------------------------
State | `StateType` (protocol) | Mostly enum, describing each state e.g. `.State0`.
Event | `EventType` (protocol) | Name for route-group. Transition can be fired via `Event` instead of explicitly targeting next `State`.
State Machine | `Machine` | State transition manager which can register `Route`/`RouteMapping` and `Handler` separately for variety of transitions.
Transition | `Transition` | `From-` and `to-` states represented as `.State1 => .State2`. Also, `.Any` can be used to represent _any state_.
Route | `Route` | `Transition` + `Condition`.
Condition | `Context -> Bool` | Closure for validating transition. If condition returns `false`, transition will fail and associated handlers will not be invoked.
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.
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.
Handler | `Context -> Void` | Transition callback invoked when state has been changed successfully.
Context | `(event: E?, fromState: S, toState: S, userInfo: Any?)` | Closure argument for `Condition` & `Handler`.
Chain | `TransitionChain` / `RouteChain` | Group of continuous routes represented as `.State1 => .State2 => .State3`


## Related Articles
Expand Down
Binary file modified Screenshots/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions Sources/Disposable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// Disposable.swift
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2014-06-02.
// Copyright (c) 2014 GitHub. All rights reserved.
//

//
// NOTE:
// This file is a partial copy from ReactiveCocoa v4.0.0-alpha.4 (removing `Atomic` dependency),
// which has not been taken out as microframework yet.
// https://github.com/ReactiveCocoa/ReactiveCocoa/issues/2579
//
// Note that `ActionDisposable` also works as `() -> ()` wrapper to help suppressing warning:
// "Expression resolved to unused function", when returned function was not used.
//

/// Represents something that can be “disposed,” usually associated with freeing
/// resources or canceling work.
public protocol Disposable {
/// Whether this disposable has been disposed already.
var disposed: Bool { get }

func dispose()
}

/// A disposable that will run an action upon disposal.
public final class ActionDisposable: Disposable {
private var action: (() -> ())?

public var disposed: Bool {
return action == nil
}

/// Initializes the disposable to run the given action upon disposal.
public init(action: () -> ()) {
self.action = action
}

public func dispose() {
self.action?()
self.action = nil
}
}
Loading