Skip to content

About

Async loading state for SwiftUI, without writing it again in every store — one macro gives you the four states, and a slow earlier load can never overwrite a newer result

Topics

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

49 Commits

Folders and files

Repository files navigation

Statable

English | 日本語

Async loading state for SwiftUI, without writing it again in every store — one macro gives you the four states, and a slow earlier load can never overwrite a newer result.

Swift 6.0+ iOS 17+ macOS 14+ License

Features

  • Declarative Macro: Reduce state management boilerplate with the @Statable macro
  • Exclusive State Representation: Type-safe expression of .idle, .loading, .loaded, .failed with AsyncState<T> enum
  • Operation Tracking: Track multiple concurrent operations individually with OperationTracker
  • @Observable Integration: Fully integrated with SwiftUI's @Observable
  • Main-actor by construction: AsyncValue and OperationTracker are @MainActor, so state transitions can never race with SwiftUI's reads — the compiler enforces it, not a comment
  • Last-started-wins: overlapping loads settle deterministically; a slow earlier load can never overwrite a newer result
  • Cancellation is not failure: a cancelled load returns to the previous value instead of showing an error or getting stuck in loading

Quick Start

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)
    }
}

Usage

Loading Data

// Basic load
await store.load {
    try await api.fetchProfile()
}

// Load only if the last load has not succeeded
await store.loadIfNeeded {
    try await api.fetchProfile()
}

Rendering

Because value returns the previous value in loading and failed, one check covers "is there anything to show", and a view renders exactly three faces:

if let value = store.value {
    Content(value)                                  // also during reload, also after a failure
    if let error = store.error { Banner(error) }    // failure does not take the screen away
} else if let error = store.error {
    FailureFace(error)                              // only when there is nothing to show
} else {
    Skeleton()                                      // no answer yet
}

Do not drive the view from isLoading alone: "is it loading" is not a reason to blank the screen.

Operation Tracking

enum DataOperation: String, CaseIterable, Sendable {
    case fetch, save, delete
}

@Statable([Item].self, operations: DataOperation.self)
@MainActor @Observable
final class ItemStore {
    public init() {}
}

struct ItemListView: View {
    @Environment(ItemStore.self) private var store

    var body: some View {
        List {
            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))
        }
    }
}

Documentation

The full API reference and the guides live in the DocC catalog Sources/Statable/Documentation.docc, including Design Principles for why the library is shaped this way.

Installation

Swift Package Manager

Add the following to your Package.swift:

dependencies: [
    .package(url: "https://github.com/no-problem-dev/swift-statable.git", from: "3.0.0")
]

Add to your target:

.target(
    name: "YourTarget",
    dependencies: [
        .product(name: "Statable", package: "swift-statable")
    ]
)

Dependencies

Package Purpose
swift-syntax Macro implementation

License

MIT License - See LICENSE for details.

About

Async loading state for SwiftUI, without writing it again in every store — one macro gives you the four states, and a slow earlier load can never overwrite a newer result

Topics

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages