Gauntlet is a swift-friendly state machine focusing on simple configuration and light weight objects. It was originally inspired by a series of blog posts over on figure.ink, but has evolved substantially since then.
- Simple Configuration
- Light Weight Objects
- Property Wrapper
- Associating Values
- Migrating from Gauntlet v4
- API
- Contributing
- License
Rather than requiring complicated graphs and configuration XML, state in Gauntlet is modeled by a single type (conforming to Transitionable
) that is capable of determining whether transitions to other instances of itself are allowed.
This not only simplifies configuration substantially, it maps very nicely onto swift's concept of an enum
and can be trivially implemented with a single switch
:
enum TrafficLight: Transitionable {
case red, yellow, green
func shouldTransition(to: TrafficLight) -> Bool {
switch (self, to) {
case (.red, .green),
(.green, .yellow),
(.yellow, .red):
return true
default:
return false
}
}
}
Gauntlet doesn’t require you to subclass objects from some abstract root machine or manage class hierarchies of behavior. Instead, a simple light weight StateMachine
class is available to compose into your types and behaviors.
A StateMachine
gets created with a Transitionable
type and an initial state. Transitioning states is a simple method call. A Combine
publisher gives subscribers the ability to react to transitions:
class MyClass {
let stateMachine = StateMachine(initialState: TrafficLight.red)
let subscription: AnyCancellable
init() {
subscription = stateMachine.publisher.sink { [weak self] _, to in
switch to {
case .red:
self?.stop()
case .yellow:
self?.slow()
case .green:
self?.go()
}
}
}
func timerTriggerd(light: TrafficLight) {
stateMachine.transition(to: light)
}
}
Becasue StateMachine
is so often used as a property, it can sometimes be more succinct to write it using property wrapper syntax. When used as a property wrapper:
- The type of the wrapped property is the state type conforming to
Transitionable
. - The default value of the wrapped property is used as the initial state of the state machine.
- Values assigned to the wrapped property pass through
transition(to:)
and are ignored if they would result in an invalid transition. - State changes are published to the “projected value” of the wrapped property — meaning we can access it by prefixing the wrapped property with a
$
.
The example above could be rewritten as:
class MyClass {
@StateMachine var stateMachine: TrafficLight = .red
let subscription: AnyCancellable
init() {
subscription = $stateMachine.sink { [weak self] _, to in
//...
}
}
func timerTriggerd(light: TrafficLight) {
stateMachine = light
}
}
An oft overlooked advantage of conforming to Transitionable
with an enum
is it allows us to easily associate values with a state:
enum Connection: Transitionable {
case fetch(URLSessionTask), success([AnyHashable: Any]), failure(Error), cancel
func shouldTransition(to: Connection) -> Bool {
switch (self, to) {
case (.fetch, .success),
(.fetch, .failure),
(_, .cancel):
return true
default:
return false
}
}
}
Values get associated when a transition is requested:
func connect() {
let task = makeTask(for: myURL) { json, error in
guard error == nil else {
stateMachine.transition(to: .faulure(error))
return
}
stateMachine.transition(to: .success(json))
}
stateMachine.transition(to: .fetch(task))
}
…And can be pulled out again when handling state changes:
subscription = stateMachine.publisher.sink { [weak self] from, to in
switch (from, to) {
case (_, .success(let json)):
self?.processJSON(json)
case (_, .failure(let error)):
self?.alert(error)
case (.fetching(let task), .cancel):
task.cancel()
default:
break
}
}
Version 5 of Gauntlet presents a number of breaking changes.
-
StateType
was deprecated last version. Now it’s dead. UseTransitionable
instead. -
Delegates were always a bit wonky in Gauntlet and have been replaced with a
Combine
pub/sub model. Thedelegates
property and itsdidTransition
member have been removed. Subscribe to thepublisher
property (or the$
projected value of a wrapped property) to get notified of state changes. -
queue(_:)
has been renamed totransition(to:)
. This is simple enough, but is indicative of a larger change… -
Timing is version 5 is subtlely different in significant ways. In version 4, state changes were queued onto the next cycle of the run loop. Then the state change and the (now obsolete)
didTransition
would run “together”. As a result, we could rely on thestate
property of the state machine and theto
argument todidTransition
to be in agreement.As of version 5, this has changed. Now state changes are applied to the state machine synchronously. But notification of these changes (via publication to the
publisher
property) still happens asynchronously (to allow for recursive transitions without overflowing the stack).As a result, the
state
of the machine is much more stable and less prone to timing-related edge cases. Yay! But we can no longer assume theto
arguments of our subscriptions topublisher
reflect the currentstate
of the machine. Boo?Subscribers will always get all state changes and will always receive them in the order they were made, so in practice I'm hoping this isn’t a big deal. But if you were relying on notifications of state change happening along side the actual change itself, it’s time to revisit those assumptions.
-
In version 4, because
state
was set asyncronously, it was surpassingly hard to test unless the transition happened to trigger some behavior observable to the test case. So Gauntlet providedwillTransition
anddidTransition
notifications that would fire ifGAUNTLET_POST_TEST_NOTIFICATIONS
was set in the environment.Now that transitions happen syncronously in version 5, these are no longer necessary and have been removed.
Full API documentation can be found here.
Pull requests are welcome! Please keep in mind this is a weekend project, though, so reviews times measured in “n of weeks” are to be expected.
Found a bug? Want a feature? Issues are the way to communicate! Though the same disclaimers around response times apply. Thank you for your patience.
Gauntlet
is released under the MIT license. See LICENSE for details.