Skip to content

Latest commit

Β 

History

206 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Swift State Graph

A graph-based reactive state management library for Swift. For managing external(escaping) state, inspired by the concepts of React, Jotai and Recoil.

Ask DeepWiki Swift 6.0+ iOS 17+

Quick Start

import StateGraph

final class Counter {
  @GraphStored var count: Int = 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

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 automatically

Works 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 StateGraphNormalization

Core Concepts

Stored Nodes (@GraphStored)

Mutable containers that hold values and notify dependents when changed:

@GraphStored var count: Int = 0
count = 10  // Dependents are notified

Computed 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 appState

UIKit 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:

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

About

πŸ‡ A next-generation graph-based state management library for SwiftUI and UIKit. Compatible with `@Observable`

Topics

Resources

Stars

68 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages