Type-safe async state machine with built-in logging and SwiftUI bindings
.package(url: "https://github.com/philiprehberger/swift-state-kit.git", from: "0.1.0")Type-safe async state machine with built-in logging and SwiftUI bindings
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/philiprehberger/swift-state-kit.git", from: "0.1.0")
]
Then add "StateKit" to your target dependencies:
.target(name: "YourTarget", dependencies: [
.product(name: "StateKit", package: "swift-state-kit")
])
import StateKit
// Define states and events
enum OrderState: Hashable, Sendable {
case pending, confirmed, shipped, delivered
}
enum OrderEvent: Hashable, Sendable {
case confirm, ship, deliver
}
// Define transitions
let machine = StateMachine(
initial: OrderState.pending,
transitions: [
Transition(from: .pending, on: .confirm, to: .confirmed),
Transition(from: .confirmed, on: .ship, to: .shipped),
Transition(from: .shipped, on: .deliver, to: .delivered)
]
)
let state = try await machine.send(.confirm) // => .confirmed
Transition(from: .pending, on: .confirm, to: .confirmed) {
try await sendConfirmationEmail()
}
let machine = StateMachine(
initial: OrderState.pending,
transitions: transitions,
logger: .console
)
// Logs: "[StateKit] pending --confirm--> confirmed"
import StateKit
// Auto-transition to error after 30 seconds in loading state
await machine.addTimeout(TimeoutTransition(
from: .loading, after: .seconds(30), on: .timeout, to: .error
))
Timeouts auto-cancel if the state changes before the duration expires.
import StateKit
// Save state
let snapshot = await machine.snapshot()
let data = try JSONEncoder().encode(snapshot)
// Restore state
let decoded = try JSONDecoder().decode(StateMachineSnapshot<OrderState>.self, from: data)
try await machine.restore(from: decoded)
import StateKit
struct AuthMiddleware: TransitionMiddleware {
func intercept(
from: OrderState, event: OrderEvent, to: OrderState,
next: @Sendable () async throws -> Void
) async throws {
guard await isAuthorized() else { throw AuthError.denied }
try await next()
}
}
await machine.addMiddleware(AuthMiddleware())
Middleware runs in order. Each must call next() to proceed or throw to reject.
import StateKit
let machine = StateMachine(initial: OrderState.pending, transitions: transitions)
await machine.onEnter(.shipped) {
try await sendTrackingNotification()
}
await machine.onExit(.pending) {
try await logOrderStart()
}
Exit actions run before the state changes, entry actions run after.
import StateKit
let machine = StateMachine(initial: OrderState.pending, transitions: transitions)
// Observe state changes reactively
Task {
for await state in await machine.stateStream {
print("State changed to: \(state)")
}
}
// Or observe full transitions
Task {
for await (from, event, to) in await machine.transitionStream {
print("\(from) --\(event)--> \(to)")
}
}
import StateKit
// Matches from any state — useful for global events like reset
let transitions = [
Transition(from: .idle, on: .start, to: .loading),
Transition(from: .loading, on: .succeed, to: .loaded),
Transition(fromAny: .reset, to: .idle) // works from any state
]
Specific transitions are always checked before wildcards.
import StateKit
let transitions = [
Transition(from: .idle, on: .start, to: .loading, guard: { await isNetworkAvailable() }),
Transition(from: .idle, on: .start, to: .error, guard: { true }) // fallback
]
When multiple transitions match the same state and event, guard conditions are evaluated in order. The first transition whose guard returns true is taken.
import StateKit
let machine = StateMachine(
initial: OrderState.pending,
transitions: transitions,
historyDepth: 0 // 0 = unlimited, nil = disabled
)
try await machine.send(.confirm)
try await machine.send(.ship)
// Inspect history
let history = await machine.history // [pending→confirmed, confirmed→shipped]
// Undo last transition
let restored = try await machine.undo() // => .confirmed
struct OrderView: View {
@State private var machine: ObservableStateMachine<OrderState, OrderEvent>?
var body: some View {
if let machine {
VStack {
Text("Status: \(machine.state)")
Button("Confirm") { Task { try await machine.send(.confirm) } }
}
}
}
}
| Method | Description |
|---|---|
init(initial:transitions:logger:historyDepth:) | Create a state machine with initial state and transitions |
send(_:) | Send an event to trigger a transition |
canSend(_:) | Check if an event is valid in the current state |
undo() | Revert to the previous state (requires history) |
onTransition(_:) | Register a callback for state changes |
onEnter(_:perform:) | Register an action for when a state is entered |
onExit(_:perform:) | Register an action for when a state is exited |
addMiddleware(_:) | Add a middleware to the transition pipeline |
reset() | Reset to initial state, clearing history |
validEvents | Set of events valid in the current state |
validEvents(for:) | Set of events valid for a given state |
validate() | Check transition table for duplicates and terminal states |
snapshot() | Create a Codable snapshot of the current state |
restore(from:) | Restore state from a snapshot |
addTimeout(_:) | Register an automatic timeout transition |
metrics | Transition metrics (if enabled) |
resetMetrics() | Reset metrics counters |
exportDOT() | Export transition graph as Graphviz DOT |
exportMermaid() | Export transition graph as Mermaid diagram |
attach(child:to:) | Attach a child state machine to a parent state |
currentState | The current state |
initialState | The initial state the machine was created with |
history | Array of past transitions |
canUndo | Whether an undo operation is available |
stateStream | AsyncStream<State> emitting new states after transitions |
transitionStream | AsyncStream of (from, event, to) tuples |
| Property | Description |
|---|---|
init(from:on:to:guard:sideEffect:) | Create a transition from a specific state |
init(fromAny:to:guard:sideEffect:) | Create a wildcard transition from any state |
from | Source state (nil for wildcard) |
event | Triggering event |
to | Destination state |
guardCondition | Optional async predicate that must return true for the transition |
sideEffect | Optional async closure executed during transition |
| Property/Method | Description |
|---|---|
init(machine:) | Create wrapper, reading initial state from the machine |
init(machine:initialState:) | Create wrapper with explicit initial state |
state | Current state (observable) |
isTransitioning | Whether a transition is in progress |
send(_:) | Send an event |
canSend(_:) | Check if an event is valid |
undo() | Revert to the previous state |
canUndo | Whether an undo operation is available |
swift build
swift test
If you find this project useful: