Learn how to manage application state in Shaft using Swift’s Observation framework.
State is any data that can change over time and affects what your UI displays. In Shaft, state management is beautifully simple thanks to Swift’s @Observable
macro.
The @Observable
macro automatically makes your classes observable, enabling automatic UI updates when properties change.
import JavaScriptKit
import Observation
import Shaft
import ShaftWeb
// Setup ShaftWeb backend for running in ShaftPad
Shaft.backend = ShaftWebBackend { _ in
JSObject.global.document.querySelector("canvas")
}
@Observable class Counter {
var count = 0
func increment() {
count += 1
}
func decrement() {
count -= 1
}
}
let counter = Counter()
class CounterView: StatelessWidget {
func build(context: any BuildContext) -> any Widget {
Column(mainAxisAlignment: .center, spacing: 16) {
Text("Count: \(counter.count)")
.textStyle(.init(fontSize: 48, fontWeight: .bold))
Row(mainAxisSize: .min, spacing: 16) {
Button {
counter.decrement()
} child: {
Text("−")
.textStyle(.init(fontSize: 24))
}
Button {
counter.increment()
} child: {
Text("+")
.textStyle(.init(fontSize: 24))
}
}
}
.center()
}
}
runApp(CounterView())
- Mark your class with
@Observable
- Read the property in your widget’s
build()
method - When the property changes, Shaft automatically rebuilds the widget
No manual subscriptions. No setState. It just works!
@Observable works perfectly with nested objects and collections:
import JavaScriptKit
import Observation
import Shaft
import ShaftWeb
// Setup ShaftWeb backend for running in ShaftPad
Shaft.backend = ShaftWebBackend { _ in
JSObject.global.document.querySelector("canvas")
}
@Observable class ShoppingList {
@Observable class Item {
var name: String
var quantity: Int
init(name: String, quantity: Int) {
self.name = name
self.quantity = quantity
}
}
var items: [Item] = [
Item(name: "Milk", quantity: 1),
Item(name: "Eggs", quantity: 12),
Item(name: "Bread", quantity: 2),
]
var total: Int {
items.reduce(0) { $0 + $1.quantity }
}
func addItem(name: String, quantity: Int) {
items.append(Item(name: name, quantity: quantity))
}
}
let shoppingList = ShoppingList()
class ShoppingView: StatelessWidget {
func build(context: any BuildContext) -> any Widget {
Column(spacing: 16) {
Text("Total Items: \(shoppingList.total)")
.textStyle(.init(fontSize: 20, fontWeight: .bold))
HorizontalDivider()
for item in shoppingList.items {
Row(spacing: 16) {
Expanded {
Text(item.name)
}
Text("\(item.quantity)")
Button {
item.quantity += 1
} child: {
Text("+")
}
}
.padding(.all(4))
}
}
.padding(.all(20))
}
}
runApp(ShoppingView())
@Observable automatically tracks changes at any depth:
- Modifying
shoppingList.items
triggers updates - Changing
item.quantity
triggers updates - The computed
total
property automatically recalculates
While @Observable is recommended, you can still use traditional StatefulWidget when needed:
import JavaScriptKit
import Shaft
import ShaftWeb
// Setup ShaftWeb backend for running in ShaftPad
Shaft.backend = ShaftWebBackend { _ in
JSObject.global.document.querySelector("canvas")
}
class Toggle: StatefulWidget {
func createState() -> ToggleState {
ToggleState()
}
}
class ToggleState: State<Toggle> {
var isOn = false
override func build(context: any BuildContext) -> any Widget {
Button {
self.setState {
self.isOn.toggle()
}
} child: {
Text(isOn ? "ON" : "OFF")
.textStyle(
.init(
color: isOn ? Color(0x00FF00) : Color(0xFF0000),
fontSize: 24
))
}
.center()
}
}
runApp(Toggle())
- Widget-level state: Button pressed state, animation state, UI-only state
- Local interactions: Toggle switches, dropdown menus, temporary UI state
- Widget-specific behavior: Focus management, scroll position, selection state
- Application state: User data, shopping cart items, authentication status
- Shared state: Data that multiple widgets need to access
- Business logic: Counter values, form data, API responses
- Global state: Settings, user preferences, app configuration