English | 日本語
A declarative state management macro for SwiftUI. Combines the AsyncValue pattern with OperationTracker to manage asynchronous state in a type-safe manner.
- Declarative Macro: Reduce state management boilerplate with the
@Statablemacro - Exclusive State Representation: Type-safe expression of
.idle,.loading,.loaded,.failedwithAsyncState<T>enum - Operation Tracking: Track multiple concurrent operations individually with
OperationTracker - @Observable Integration: Fully integrated with SwiftUI's
@Observable - Sendable Conformance: Full Strict Concurrency support
import SwiftUI
import Statable
// Simple Store definition
@Statable(MetabolicProfile.self)
@MainActor @Observable
final class ProfileStore {
public init() {}
var currentAge: Int { value?.age() ?? 0 }
}
// Store with operation tracking
enum WorkoutOperation: String, CaseIterable, Sendable {
case fetch, recordStrength, recordCardio
}
@Statable([WorkoutActivity].self, operations: WorkoutOperation.self)
@MainActor @Observable
final class WorkoutStore {
public init() {}
var isRecording: Bool {
operations.isActive(.recordStrength) || operations.isActive(.recordCardio)
}
}Add the following to your Package.swift:
dependencies: [
.package(url: "https://github.com/no-problem-dev/swift-statable.git", from: "1.0.2")
]Add to your target:
.target(
name: "YourTarget",
dependencies: [
.product(name: "Statable", package: "swift-statable")
]
)@Statable(UserProfile.self)
@MainActor @Observable
final class UserStore {
public init() {}
}
// Usage in View
struct ProfileView: View {
@Environment(UserStore.self) private var store
var body: some View {
switch store.state {
case .idle:
Text("No data")
case .loading(let previous):
VStack {
ProgressView()
if let prev = previous {
Text("Previous: \(prev.name)")
}
}
case .loaded(let profile):
Text("Hello, \(profile.name)")
case .failed(let error):
Text("Error: \(error.localizedMessage)")
}
}
}// Basic load
await store.load {
try await api.fetchProfile()
}
// Load only if no value exists
await store.loadIfNeeded {
try await api.fetchProfile()
}
// Force reload
await store.reload {
try await api.fetchProfile()
}enum DataOperation: String, CaseIterable, Sendable {
case fetch, save, delete
}
@Statable([Item].self, operations: DataOperation.self)
@MainActor @Observable
final class ItemStore {
public init() {}
}
// Tracking operations
struct ItemListView: View {
@Environment(ItemStore.self) private var store
var body: some View {
List {
if store.operations.isActive(.fetch) {
ProgressView("Loading...")
}
ForEach(store.value ?? []) { item in
ItemRow(item: item)
}
}
.toolbar {
Button("Save") {
Task {
await store.operations.run(.save) {
try await api.saveItems(store.value ?? [])
}
}
}
.disabled(store.operations.isActive(.save))
}
}
}| Property | Type | Description |
|---|---|---|
value |
T? |
Current value |
state |
AsyncState<T> |
State (for switch) |
isLoading |
Bool |
Whether loading |
isIdle |
Bool |
Whether idle |
isFailed |
Bool |
Whether failed |
hasValue |
Bool |
Whether value exists |
error |
StateError? |
Error |
operations |
OperationTracker<Op> |
Operation tracker (only with operations argument) |
| Method | Description |
|---|---|
set(_:) |
Set value |
setError(_:) |
Set error |
startLoading() |
Start loading |
reset() |
Reset to initial state |
load(_:) |
Execute async operation |
loadIfNeeded(_:) |
Load only if no value |
reload(_:) |
Force reload |
public enum AsyncState<Value: Sendable>: Sendable {
case idle // Initial state
case loading(previous: Value?) // Loading (retains previous value)
case loaded(Value) // Load succeeded
case failed(StateError) // Load failed
}// Start/complete operations
operations.start(.fetch)
operations.complete(.fetch)
operations.fail(.fetch, with: error)
// Check state
operations.isActive(.fetch)
operations.hasActiveOperations
operations.error(for: .fetch)
// Convenience method
await operations.run(.fetch) {
try await api.fetchData()
}public enum StateError: Error, Sendable, Equatable, Hashable {
case network(NetworkError)
case validation(ValidationError)
case notFound(resource: String)
case unauthorized
case server(code: Int, message: String)
case unknown(String)
}
// Convenience properties
error.localizedMessage // User-facing message
error.isRetryable // Whether retry is appropriate
// Convert from standard Error
let stateError = StateError(from: someError)Each Store manages a single type of async value. This ensures:
- State consistency
- Easy testing
- Clear responsibilities
The AsyncState enum represents exclusive states, preventing contradictory states (e.g., isLoading = true AND error != nil) at the type level.
loading(previous: Value?) allows displaying the previous value during reload, improving UX.
Detailed API documentation is available on GitHub Pages.
| Package | Purpose |
|---|---|
| swift-syntax | Macro implementation |
MIT License - See LICENSE for details.