Skip to content

Repository files navigation

Oak - Swift Finite State Machine Library

Oak FrameworkSwift 6.2PlatformsiOS 15.0+macOS 12.0+Swift Package ManagerLicense

A type-safe, asynchronous finite state machine implementation for Swift, with powerful effect handling and SwiftUI integration.

Development Status

Oak is actively developed and evolving. The library is well-tested and functional, with a stable core API.

We encourage developers to try Oak, provide feedback, and contribute to its evolution.

Overview

What Oak is:

  • A Swift library for modeling application logic as finite-state machines (transducers).
  • A unidirectional, event-driven, state-and-effect system with strong type safety.
  • Clear separation of pure state transition logic from side effects (via Effects).
  • A composable architecture that integrates with SwiftUI.
  • A runtime that manages async work (managed tasks) and cancellation in a lifecycle-aware manner.

An advanced Example: Effect Counter Transducer

The example demonstrates how to define a state machine which uses effects:

Show Example
import Oak
// Simple, non-terminating counter state machine with // async effects
enumEffectCounter:EffectTransducer{
// State holds counter value and tracks operation // progress
structState:NonTerminal{enumPending{case none
case increment
case decrement
}varvalue:Int=0varpending:Pending=.none
varisPending:Bool{ pending !=.none }}staticvarinitialState:State{State()}
// Dependencies needed by effects
structEnv:Sendable{init(){self.serviceIncrement ={tryawaitTask.sleep(for:.seconds(1))}self.serviceDecrement ={tryawaitTask.sleep(for:.seconds(1))}}varserviceIncrement:@Sendable()asyncthrows->VoidvarserviceDecrement:@Sendable()asyncthrows->Void}
// Events that trigger state transitions
enumEvent{case increment
case decrement
case reset
case incrementReady
case decrementReady
}
// Effect for increment: creates an operation effect
staticfunc incrementEffect()->Self.Effect{Effect{ env, input intryawait env.serviceIncrement()try input.send(.incrementReady)}}
// Effect for decrement: creates an operation effect
staticfunc decrementEffect()->Self.Effect{Effect(id:"decrement"){ env, input intryawait env.serviceDecrement()try input.send(.decrementReady)}}
// Core state transition logic: a pure function that // handles events and returns effects
staticfunc update(
_ state:inoutState, event:Event)->Self.Effect?{switch(state.pending, event){case(.none,.increment):
state.pending =.increment
returnincrementEffect()case(.none,.decrement):
state.pending =.decrement
returndecrementEffect()case(.none,.reset):
state =State(value:0, pending:.none)returnnilcase(.increment,.incrementReady):
state.value +=1
state.pending =.none
returnnilcase(.decrement,.decrementReady):
state.value -=1
state.pending =.none
returnnil
// Ignore increment/decrement events during // pending operation
case(_,.increment),(_,.decrement):returnnilcase(_,.reset):
state =State(value:0, pending:.none)returnnildefault:returnnil}}}

Features

  • Type-safety: Leverage Swift's type system to catch invalid usage at compile time
  • Actor isolation: Safe execution across different isolation contexts with automatic inference
  • Side effect management: Structured handling of asynchronous side effects with proper cancellation
  • Environment support: Provide typed environment to effects for dependencies and configuration
  • Async/await support: Built on Swift's structured concurrency model
  • SwiftUI integration: Reactive bindings with TransducerView
  • Composability: Pipe transducer outputs into other transducer inputs
  • Effect composition: Combine multiple effects sequentially or in parallel
  • Back pressure support: In compositions, a connected output awaits readiness of the consumer
  • Completion callbacks: Handle transducer completion with type-safe callbacks

SwiftUI Integration

TransducerView

TransducerView is a SwiftUI view that integrates transducers directly into your view hierarchy. It manages the transducer's lifecycle, automatically starting it when the view appears and cleaning up when the view disappears. The view reactively updates whenever the transducer's state changes. A TransducerView directly uses a view's @State as the transducer's state and utilizes SwiftUI's built-in diffing facility for efficient updates.

As a Transducer Actor, TransducerView combines the power of transducers with effect handling and the composability of SwiftUI views, enabling hierarchical transducer architectures where parent and child views can each manage their own state machines while participating in a coordinated view hierarchy.

Note: TransducerView can replace conventional ViewModel implementations using ObservableObject or the Observation framework. This supports a "View only architecture" where traditional artifacts like Model, ViewModel, Router, and Interactor are consolidated and implemented directly as SwiftUI views.

Basic Usage

structCounterView:View{@Stateprivatevarstate=SimpleCounter.State()varbody:someView{TransducerView(
of:SimpleCounter.self,
initialState: $state
){ state, input inVStack{Text("Count: \(state.count)")Button("Increment"){try? input.send(.increment)}Button("Decrement"){try? input.send(.decrement)}}}}}

With Output Handling and Environment for Effects

Show Example
extensionEnvironmentValues{@EntryvareffectCounterEnv:EffectCounter.Env=.init()}structContentView:View{@Environment(\.effectCounterEnv)varenv@Stateprivatevarstate=EffectCounter.initialState
@StateprivatevarlastOutput:String=""varbody:someView{TransducerView(
of:EffectCounter.self,
initialState: $state,
env: env,
output:Callback{ output in
lastOutput ="Last action: \(output)"}){ state, input inVStack{Text("Count: \(state.count)")Text(lastOutput)Button("Increment"){try? input.send(.increment)}}}}}

Completion Callbacks

Show Example Handle transducer completion with type-safe callbacks that are called when the transducer finishes:
structContentView:View{@SwiftUI.Stateprivatevarstate=MyTransducer.State()varbody:someView{TransducerView(
of:MyTransducer.self,
initialState: $state,
completion:{ result inswitch result {case.success(let output):print("Transducer completed successfully with output: \(output)")case.failure(let error):print("Transducer failed with error: \(error)")}}){ state, input in
// UI content
}}}

Note: Completion callbacks are always invoked when the transducer finishes, whether it completes successfully or encounters an error. The callback receives a Result that contains either the success value or the error.

ObservableTransducer

ObservableTransducer is an @Observable class that wraps a transducer for use outside of SwiftUI views or when you need to share transducer state across multiple views. It provides reactive state management using the Observation framework, making it perfect for ViewModels or standalone state management.

Direct Usage in SwiftUI

All you need to do to support this pattern is to instantiate the Observable from the generic type ObservableTransducer and assign it a property in the view. You don't need to create the Observable class yourself anymore.

structCounterView:View{@Stateprivatevarcounter=ObservableTransducer(
of:CounterTransducer.self,
initialState:CounterTransducer.State(),
env:CounterTransducer.Env())varbody:someView{VStack{Text("Count: \(counter.state.count)")Button("Increment"){try? counter.proxy.send(.increment)}Button("Decrement"){try? counter.proxy.send(.decrement)}}}}

Note: The same Transducer definition, that is a type conforming to either Transducer or EffectTransducer, can be used for a TransducerView and/or for an ObservableTransducer.

Transducer Composition

Oak includes experimental support for composing transducers at the type level. This advanced feature allows for the creation of composite state machines that maintain the type-safety guarantees of the component transducers.

This composition mechanism is still evolving, but it shows promise for building complex state management solutions with clean architectural boundaries. Various composition strategies (parallel, sequential, custom) are being explored to determine the most effective patterns for different use cases.

Installation

Swift Package Manager

In Xcode, select File → Add Packages… and enter:

https://github.com/couchdeveloper/Oak.git

Or add to your Package.swift:

dependencies:[.package(url:"https://github.com/couchdeveloper/Oak.git", from:"0.28.0")]

Quick Start

Oak uses transducers - finite state machines that process events and produce outputs. Here's how to create a simple counter transducer returning the counter's value as its output:

1. Define Your Transducer

import Oak
// A basic counter transducer that outputs the current count.
enumSimpleCounter:Transducer{structState:NonTerminal{varcount:Int=0}staticvarinitialState:State{State()}enumEvent{case increment
case decrement
}enumOutput{case none
case value(Int)}staticfunc update(_ state:inoutState, event:Event)->Output{switch event {case.increment:
state.count +=1return.value(state.count)case.decrement:
state.count -=1return.value(state.count)}}}

2. Run Your Transducer

letproxy=SimpleCounter.Proxy()lettask=Task{tryawaitSimpleCounter.run(
initialState:SimpleCounter.initialState,
proxy: proxy
)}
// Send events
try proxy.send(.increment)try proxy.send(.decrement)

3. Use in SwiftUI

To demonstrate the usage in SwiftUI, the counter transducer will be used using a TransducerView. The TransducerView has a Content view which receives the current state and an "Input" for the transducer. The content view is responsible to render the state and send user intents to the transducer via the input.

In addition, the counter value can be optionally observed with a Callback which calls a closure that receives the current counter value:

import SwiftUI
structContentView:View{@SwiftUI.Stateprivatevarstate=SimpleCounter.initialState
varbody:someView{TransducerView(
of:SimpleCounter.self,
initialState: $state,
output:Callback{ output inswitch output {case.none:breakcase.value(let count):print("Count updated to: \(count)")}}){ state, input inVStack{Text("Count: \(state.count)")Button("Increment"){try? input.send(.increment)}Button("Decrement"){try? input.send(.decrement)}}}}}

This design allows the enclosing view, that is the ContentView in this example, to observe the output and also the transducer's state to react on it. It also allows the ContentView to orchestrate more than one transducer views in more complex scenarios.

If orchestrating becomes more complex, the ContentView may itself utilize a transducer that handles the state transitions and it will become the content view of it. That way, complex hierarchies can be built to solve complex UI scenarios.

Ready for more? Check out the Core Concepts section below!

Core Concepts

Transducer

A finite state machine that processes events and produces outputs with optional side effects.

Transducers are implemented as async throwing functions rather than classes with mutable state. This design enables better composability since functions can be composed more easily than objects, and eliminates the need for object lifecycle management.

Proxy

A proxy provides an asynchronous event channel for sending events to the transducer and letting the transducer obtain these events in an async loop. A proxy is required to start a transducer:

letproxy=CounterTransducer.Proxy()tryawaitCounterTransducer.run(
initialState:CounterTransducer.initialState,
proxy: proxy
)

It can also be used to send events and to forcibly cancel the transducer:

try proxy.send(.increment)
proxy.cancel()

State

The state of the finite state machine must conform to Terminable.

A transducer can run with strictly encapsulated state or with shared state defined in an actor. For strictly encapsulated state, the initial state value is passed to the run function. If the state is shared, a binding is used, allowing external components to observe state changes.

structState:Terminable{varvalue:Int=0varisTerminal:Bool{ value >10}}

For non-terminal states, conform to NonTerminal:

structState:NonTerminal{varvalue:Int=0}

Event

Input values that trigger state transitions in the finite state machine.

Events represent things that happen in your system - user actions, network responses, timer events, etc. They are processed by the transducer's update function to produce state changes and effects.

enumEvent{case userTapped
case dataReceived(Data)case timeout
}

Input

Represents the input channel of a transducer. A proxy has an input property which provides access to the transducer's input channel.

An input is used to send events into the transducer. It's passed as a parameter to effect operations and can be shared with other components. Components that only have access to the input interface cannot forcibly cancel the transducer.

Effect(isolatedOperation:{ _, input, _ intryawaitTask.sleep(nanoseconds:1_000_000_000)try input.send(.ready)})

Output

Values produced by the transducer's update function that can be observed externally, enabling communication with other components or UI updates.

typealiasOutput=Intstaticfunc update(_ state:inoutState, event:Event)->Output{
state.counter +=1return state.counter // Output value
}

Effect

An effect is a special output of an effect transducer. It encapsulates an asynchronous operation that can perform side effects and send events back to the transducer.

typealias Env {}staticfunc update(_ state:inoutState, event:Event)->Self.Effect?{
// return an effect or nil
}

Effects come in two forms:

  • Actions: Synchronous functions that return events immediately
  • Operations: Long-running, cancellable tasks that send events asynchronously

Action Effect:

staticfunc createDelegate(param:Param)->Effect{letdelegate=MyDelegate(param: param)return.event(.delegate(delegate))}

Operation Effect:

staticfunc incrementEffect()->Effect{Effect(isolatedOperation:{ _, input, _ intryawaitTask.sleep(nanoseconds:1_000_000_000)try input.send(.incrementReady)})}

For global actor isolation:

staticfunc incrementEffect()->Effect{Effect(operation:{@MyGlobalActor _, input intryawaitTask.sleep(nanoseconds:1_000_000_000)try input.send(.incrementReady)})}

Subject

A component that receives output values from a transducer, enabling communication with other parts of your application.

letoutput= Callback<Int>{ value inprint("Received output: \(value)")}tryawaitCounterTransducer.run(
initialState: state,
proxy: proxy,
output: output
)

Design Philosophy

Oak makes state management explicit, predictable, and ergonomic while embracing Swift's modern concurrency features.

License

Apache License (v2.0) - See LICENSE file for details

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

AI Guidelines

For contributors using AI coding assistants, see AI_GUIDELINES.md for concise, tool-agnostic rules tailored to this repository. It complements .github/copilot-instructions.md with norms on effects, terminal states, concurrency, and testing. Start here: AI_GUIDELINES.md.

About

A Finite State Transducer library in Swift

Resources

Stars

8 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages