StateTree is an experimental Swift framework that models an application's domain as a tree of value-type nodes, kept separate from its user interface. Business logic, navigation, side effects, and state are expressed as a single serializable, replayable model. The user interface — SwiftUI, UIKit, AppKit, or none — observes that model rather than housing it.
StateTree was written in 2023, before Swift's
@Observablemacro and the Observation framework, and before the language's concurrency model had settled. It investigates how far a fully UI-decoupled, serializable, reactive domain model could be taken in the Swift of that period, and so provides its own observation layer based on a hand-rolled ReactiveX implementation, alongside its own dependency injection, routing, and side-effect systems.Some of the problems it addresses were later resolved at the language and SDK level. Others remain absent from any common production systems.
It is best read as an exploration of the questions "Why is everything centered around the UI — when doing so inherently prevents re-use?" and "What can we do about it?"
In considering the former question, it's worth noting that platfrom companies are strongly disincentivized to help make their end-user-experiences a commodity.
Most application architectures bind domain logic to the view layer: to View
structs, view controllers, or view models that exist primarily to feed a screen.
StateTree inverts that relationship. The application is a tree of Nodes, each a
plain struct describing one part of the domain, and the user interface is an
optional, interchangeable projection of that tree.
A Node looks rather like a SwiftUI View. However, rather than describing UI in a body,
it declares its persistent state, children, dependencies, and behavior through a declarative rules result builder.
import StateTree
struct Counter: Node, Identifiable {
let id: Int
let shouldDelete: () -> Void
@Value var count: Int = 0 // observable, persisted state
var rules: some Rules {
OnUpdate(count) { _ in // declarative lifecycle reaction
count = max(min(10, count), -10)
}
}
func increment() { count += 1 }
func decrement() { count -= 1 }
func delete() { shouldDelete() }
}Parent nodes compose their children declaratively by routing over data, so the shape of the tree follows from state rather than from imperative wiring:
struct CountersList: Node {
@Value private var counterIDs: [Int] = []
@Route var counters: [Counter] = []
var rules: some Rules {
Serve(data: counterIDs, at: $counters) { id in
Counter(id: id, shouldDelete: { delete(counter: id) })
}
}
}A Tree<RootNode> runtime hosts the root node, propagates changes through a
priority-queue update engine, and exposes the resulting state in serializable
form.
| Concept | What it is |
|---|---|
Node |
A value-type (struct) unit of domain state and behavior. Declares a rules builder. |
@Value |
Observable, persisted state held in the runtime — the source of truth. |
@Route |
Declares child node(s) and gives the tree its structure. Single, list, and 2/3-way union routers are supported. |
@Projection |
A two-way passthrough into state owned elsewhere in the tree. Equivalent to SwiftUI's @Binding. |
@Dependency |
Environment-style dependency injection, resolved down the tree. |
@Scope |
Access to the runtime for transactions and structural operations. |
| Rules | A @RuleBuilder DSL: routing (Serve), lifecycle (OnStart, OnStop, OnUpdate, OnChange, OnReceive), intents (OnIntent), and Inject. |
| Behaviors | Managed, trackable side effects (sync, async, or streamed) with interceptors — awaitable and mockable in tests. |
| Intents | URL-encodable, resumable, multi-step actions that flow down the tree to drive navigation and deep linking. |
- UI independence. The same domain tree can drive SwiftUI, UIKit, AppKit, a test harness, or a headless process. The interface can be replaced or omitted.
- Whole-app serialization. The runtime reduces the live tree to a
CodableTreeStateRecord. State can be captured as JSON, persisted, and used to start the application from a saved point (tree.start(from:)). - Time-travel debugging.
StateTreePlaybackrecords and replays state frames (Recorder,Player,StateFrame), with a SwiftUIPlaybackViewprovided. - Testable side effects. Behaviors are tracked and interceptable, which makes asynchronous work deterministic and awaitable in tests.
- Deep-linkable navigation. Intents are URL-encoded and resolved step by step by the nodes along a route.
- Structured concurrency. All tree access is isolated to the
@TreeActorglobal actor — aliased to@MainActorby default and replaceable with a custom actor — with Swift's strict concurrency checking enabled.
| Module | Purpose |
|---|---|
StateTree |
The core runtime, nodes, fields, rules, routing, and serialization. |
StateTreeSwiftUI |
SwiftUI bridge: @TreeRoot, @TreeNode, projections to Bindings, PlaybackView. |
StateTreeImperativeUI |
Bridge for imperative UIKit and AppKit hosts. |
StateTreePlayback |
Recording and replay of tree state for time-travel debugging. |
StateTreeTesting |
Test utilities for driving and asserting on trees. |
In SwiftUI, the tree is mounted with a single property wrapper, and nodes are
exposed to views as observable values with Binding projections:
@main
struct CounterApp: App {
@TreeRoot var root = CountersList()
var body: some Scene {
WindowGroup { CountersListView(list: $root.node) }
}
}
struct CounterView: View {
@TreeNode var counter: Counter
var body: some View {
Stepper("\(counter.count)",
onIncrement: counter.increment,
onDecrement: counter.decrement)
}
}- Swift 5.8+ toolchain, with concurrency upcoming-features enabled.
- macOS 12.3+ / iOS 15.4+ for the SwiftUI module.
- SwiftUI is optional. The core builds without it — on Linux, for example — and the SwiftUI targets are excluded automatically in that case.
The package depends on
swift-collections and on two
internal libraries under Vendor/: Emitter (reactive streams) and Disposable
(lifetime management).
The Examples/ directory contains three complete apps that share their domain
logic across different UI styles:
- Counter — a UIKit/AppKit app built on
StateTreeImperativeUI. - ToDo — a SwiftUI app using
NavigationSplitView, routed selection, and state booted from a bundled JSON snapshot. - TicTacToe — game logic modelled as a node tree.
StateTree is complete. It reached its goals on 2023-06-08 with v0.1.0 — by
coincidence the same week @Observable was introduced at WWDC 2023.
It is preserved as a research artifact, not maintained as a production dependency.
Were the work continued in Swift, its foundations would be rebuilt on what the language
and SDK have since provided: @Observable and the Observation framework, and the now-mature
concurrency and macro features.
On 2026-06-03 the project was updated the project to build and run under the Swift 6.3 toolchain.
MIT © 2023 GOOD HATS LLC (Adam Zethraeus). See LICENSE.