vergegroup/swift-state-graph
A graph-based reactive state management library for Swift.
Quick Start
import StateGraph
final class Counter {
@GraphStored var count = 0
@GraphComputed var isEven: Bool
init() {
$isEven = .init { [$count] _ in
$count.wrappedValue % 2 == 0
}
}
}
// Usage
let counter = Counter()
counter.count = 5
print(counter.isEven) // false - automatically computed@GraphStored can infer its type from an initial value. Keep an explicit type when there is no initial value or when the expression needs type context, such as nil, .init(), or an empty collection.
Why Swift State Graph?
Automatic Dependency Tracking
Computed properties automatically track their dependencies and update when source values change:
@GraphStored var firstName: String = "John"
@GraphStored var lastName: String = "Doe"
@GraphComputed var fullName: String
init() {
$fullName = .init { [$firstName, $lastName] _ in
"\($firstName.wrappedValue) \($lastName.wrappedValue)"
}
}
// Change firstName → fullName updates automaticallyWorks with SwiftUI and UIKit
Native integration with SwiftUI's reactive system and UIKit through tracking APIs:
// SwiftUI - just use the properties
struct CounterView: View {
let counter: Counter
var body: some View {
Text("\(counter.count)")
Button("Up") { counter.count += 1 }
}
}
// UIKit - use withGraphTracking
subscription = withGraphTracking {
withGraphTrackingGroup {
print(counter.count)
}
}Drop-in Observable Enhancement
Migrate from @Observable and gain automatic computed property updates:
// Before: Manual validation updates
@Observable class UserViewModel {
var name = ""
var isValid = false
func validate() { isValid = !name.isEmpty }
}
// After: Automatic reactivity
final class UserViewModel {
@GraphStored var name: String = ""
@GraphComputed var isValid: Bool
init() {
$isValid = .init { [$name] _ in !$name.wrappedValue.isEmpty }
}
}UserDefaults Composition
Persist a graph-aware value without changing the Stored primitive:
@GraphUserDefault("theme")
var theme: String = "light"The projected value, $theme, is the reference-identity GraphUserDefault<String> handle. Its reads participate in graph dependency tracking, and its writes persist to UserDefaults.
Installation
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/VergeGroup/swift-state-graph.git", from: "1.0.0")
].target(
name: "YourTarget",
dependencies: ["StateGraph"]
)Dynamic Linking
StateGraph and StateGraphNormalization are distributed as dynamic library products. StateGraph owns process-wide tracking and transaction context, so one process must load one shared StateGraph runtime. Dynamic products ensure that multiple frameworks link to shared binaries instead of embedding independent static copies.
The application target should embed the produced frameworks. Dependent dynamic frameworks should link them without embedding additional copies.
With Tuist's native Swift Package integration, use the default .runtime dependency from each feature framework and .runtimeEmbedded once from the final application target:
// Feature framework
.package(product: "StateGraph")
// Application
.package(product: "StateGraph", type: .runtimeEmbedded)
.package(product: "StateGraphNormalization", type: .runtimeEmbedded)StateGraphNormalization does not re-export StateGraph. Targets using graph nodes together with normalization must depend on and import both products explicitly:
import StateGraph
import StateGraphNormalizationCore Concepts
Stored Nodes (@GraphStored)
Mutable containers that hold values and notify dependents when changed:
@GraphStored var count: Int = 0
count = 10 // Dependents are notifiedComputed Nodes (@GraphComputed)
Read-only values derived from other nodes. They:
- Automatically track dependencies
- Recalculate lazily when dependencies change
- Cache results until invalidated
@GraphComputed var doubled: Int
$doubled = .init { [$count] _ in $count.wrappedValue * 2 }Reactive Tracking
Observe changes without SwiftUI:
let subscription = withGraphTracking {
withGraphTrackingGroup {
print("Changed: \(model.count), \(model.name)")
}
}SwiftUI Integration
Basic Usage
Properties are automatically observed in SwiftUI views:
struct ItemListView: View {
let store: ItemStore
var body: some View {
List(store.items) { item in
Text(item.name)
}
TextField("New Item", text: store.$newItemName.binding)
}
}Lifecycle Tracking
Use .graphTracking when a SwiftUI view needs to keep a withGraphTracking subscription alive only while the view is visible:
struct AutoRefreshView: View {
let model: RefreshModel
var body: some View {
Toggle("Auto Refresh", isOn: model.$isEnabled.binding)
.graphTracking {
withGraphTrackingMap {
model.isEnabled
} onChange: { isEnabled in
if isEnabled {
model.start()
} else {
model.stop()
}
}
}
}
}Environment Integration
Use GraphObject protocol for environment propagation:
final class AppState: GraphObject {
@GraphStored var user: User?
@GraphComputed var isLoggedIn: Bool
init() {
$isLoggedIn = .init { [$user] _ in $user.wrappedValue != nil }
}
}
// Inject
ContentView().environment(appState)
// Access
@Environment(AppState.self) private var appStateUIKit Integration
Use withGraphTracking to observe state changes:
class ViewController: UIViewController {
private let viewModel = ViewModel()
private var subscription: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
subscription = withGraphTracking {
withGraphTrackingMap {
viewModel.items
} onChange: { [weak self] items in
self?.tableView.reloadData()
}
}
}
}Documentation
For detailed guides, see the Documentation:
- Core Concepts - Stored, Computed, and dependency tracking
- SwiftUI Integration - Bindings, GraphObject, Environment
- UIKit Integration - withGraphTracking patterns
- UserDefaults - Persistent values composed with Stored nodes
- Data Normalization - EntityStore for relational data
- Migration from Observable - Step-by-step guide
Advanced Topics
Nested Tracking
Groups and Maps can be nested. When a parent re-executes, nested children are automatically cancelled and recreated:
withGraphTrackingGroup {
if viewModel.featureEnabled {
withGraphTrackingGroup {
// Only tracked when featureEnabled is true
print("Feature value: \(viewModel.featureValue)")
}
}
}State Sharing
Share state between objects by assigning @GraphStored references:
final class ViewModel {
@GraphStored var items: [Item]
init(service: DataService) {
$items = service.$items // Shares the same node
}
}Requirements
- Swift 6.0+
- iOS 17+ / macOS 14+
License
MIT License
Package Metadata
Repository: vergegroup/swift-state-graph
Default branch: main
README: README.md