Skip to content
Merged

3.0.0 #148

Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4fdbf41
Add SwiftData integration (ModelContainer dependency + ModelState)
claude Jun 9, 2026
549c527
Raise platform floors to iOS 17 / macOS 14 and pin Swift 6 language mode
claude Jun 9, 2026
f5c8719
CI: treat warnings as errors to enforce strict Swift 6 cleanliness
claude Jun 9, 2026
acb5c0d
Adopt explicit existentials and enable the ExistentialAny upcoming fe…
claude Jun 9, 2026
79688e3
Migrate reactivity from ObservableObject to the Observation framework
claude Jun 9, 2026
4ee984a
Fix Observation migration leftovers on Apple platforms
claude Jun 9, 2026
a3fbb09
Docs: add AppState 3.0 upgrade guide
claude Jun 9, 2026
21d7fbf
Docs: translate v3 guides and propagate notifyChange across all langu…
claude Jun 9, 2026
bab8ef9
Add Observation reactivity test for the @Observable bridge
claude Jun 9, 2026
f69fbfb
Add fledge task runner and spec-sync setup (application + swiftdata s…
claude Jun 9, 2026
72b2f6c
spec-sync: author the property-wrappers spec for v3
claude Jun 9, 2026
29c889e
Polish: align ModelState logging and doc wording with house style
claude Jun 9, 2026
db88f9a
Address review: ModelState API, observation isolation, scoped strict CI
claude Jun 9, 2026
e6f50f0
Keep observation anchor nonisolated; document the concurrency invariant
claude Jun 9, 2026
9104788
Docs/specs: sync ModelState references to read-only models / deleteAll()
claude Jun 9, 2026
faf5e33
Docs: justify the @Observable migration; final spec sync
claude Jun 9, 2026
50465e6
Merge pull request #1 from corvid-agent/claude/swiftdata-appstate-int…
corvid-agent Jun 9, 2026
060c649
Fix: address PR #147 review findings
0xLeif Jun 9, 2026
4dc85a7
Add: six SwiftUI example packages + Examples CI
0xLeif Jun 9, 2026
811bc75
Test: drive example SwiftUI to 100% coverage with ViewInspector
0xLeif Jun 10, 2026
7087ae2
Test: expand SwiftData + Observation (3.0.0) test coverage
0xLeif Jun 10, 2026
1f0ce52
Add: runnable demo app, advanced SwiftData example, adversarial test …
0xLeif Jun 10, 2026
31532a3
Fix: correctness bugs surfaced by the adversarial suite
0xLeif Jun 10, 2026
23ad72a
Test: add XCUITest UI suite for the demo app
0xLeif Jun 10, 2026
df0ac7c
Add: non-blocking background SwiftData + responsive stress UI
0xLeif Jun 10, 2026
2a93664
CI: fix cross-platform + SDK-version build failures
0xLeif Jun 10, 2026
83c5bd0
CI: guard adversarial suite to Apple platforms; fix toolchain mismatch
0xLeif Jun 10, 2026
330cb7c
Refactor: make Keychain checked-Sendable (drop @unchecked)
0xLeif Jun 10, 2026
40f854e
Remove examples and ViewInspector to keep 3.0.0 a lean library
0xLeif Jun 10, 2026
a8477fc
Docs + observation: finalize 3.0 docs; single observation path for State
0xLeif Jun 11, 2026
cbf5841
Docs: regenerate de/es/fr/hi/pt/ru/zh-CN translations to match the 3.…
0xLeif Jun 11, 2026
2cfca62
CI: fix DocC Pages deploy to use latest-stable Xcode on macos-15
0xLeif Jun 11, 2026
bce4628
Address #148 review: ModelState strict throwing API + main-thread dep…
0xLeif Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Examples

on:
push:
branches: ["**"]
paths:
- 'Examples/**'
- 'Sources/**'
- 'Package.swift'
- '.github/workflows/examples.yml'
pull_request:
paths:
- 'Examples/**'
- 'Sources/**'
- 'Package.swift'
- '.github/workflows/examples.yml'

jobs:
test-moderate-examples:
name: Test Moderate Examples
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 16.2
- name: Set up Swift
uses: swift-actions/setup-swift@v2
with:
swift-version: '6.2'

- name: Test TodoCloud
working-directory: Examples/Moderate/TodoCloud
run: swift test -v

- name: Test SettingsKit
working-directory: Examples/Moderate/SettingsKit
run: swift test -v

- name: Test DataDashboard
working-directory: Examples/Moderate/DataDashboard
run: swift test -v

- name: Test SecureVault
working-directory: Examples/Moderate/SecureVault
run: swift test -v

test-focused-examples:
name: Test Focused Examples
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 16.2
- name: Set up Swift
uses: swift-actions/setup-swift@v2
with:
swift-version: '6.2'

- name: Test SyncNotes
working-directory: Examples/Focused/SyncNotes
run: swift test -v

- name: Test MultiPlatformTracker
working-directory: Examples/Focused/MultiPlatformTracker
run: swift test -v

test-swiftdata-example:
name: Test SwiftData Example
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 16.2
- name: Set up Swift
uses: swift-actions/setup-swift@v2
with:
swift-version: '6.2'

- name: Test SwiftDataExample
working-directory: Examples/SwiftDataExample
run: swift test -v
10 changes: 10 additions & 0 deletions .github/workflows/macOS.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,15 @@ jobs:
- uses: actions/checkout@v4
- name: Build
run: swift build -v
env:
APPSTATE_STRICT: "1"
- name: Run tests
run: swift test -v
env:
APPSTATE_STRICT: "1"
- name: Build SwiftData example
run: swift build -v
working-directory: Examples/SwiftDataExample
- name: Run SwiftData example
run: swift run
working-directory: Examples/SwiftDataExample
4 changes: 4 additions & 0 deletions .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@ jobs:
- uses: actions/checkout@v4
- name: Build for release
run: swift build -v -c release
env:
APPSTATE_STRICT: "1"
- name: Test
run: swift test -v
env:
APPSTATE_STRICT: "1"
10 changes: 8 additions & 2 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@ jobs:

# ② Build & test
- run: swift --version # sanity-check
- run: swift build
- run: swift test
- name: Build
run: swift build
env:
APPSTATE_STRICT: "1"
- name: Test
run: swift test
env:
APPSTATE_STRICT: "1"
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
.DS_Store
/.build
.build/
/Packages
xcuserdata/
DerivedData/
.swiftpm/
.netrc
Package.resolved
Package.resolved
# xcodegen-generated demo project + derived data
Examples/DemoApp/*.xcodeproj/
Examples/DemoApp/.build-dd/
3 changes: 3 additions & 0 deletions .specsync/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
backup-3x/
config.local.toml
hashes.json
12 changes: 12 additions & 0 deletions .specsync/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# spec-sync v4 configuration
# Docs: https://github.com/CorvidLabs/spec-sync

specs_dir = "specs"
source_dirs = ["Sources"]
exclude_dirs = []
exclude_patterns = []
required_sections = ["Purpose", "Public API", "Invariants", "Behavioral Examples", "Error Cases", "Dependencies", "Change Log"]
enforcement = "strict"

[lifecycle]
track_history = false
7 changes: 7 additions & 0 deletions .specsync/registry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[registry]
name = "AppState"

[specs]
application = "specs/application/application.spec.md"
property-wrappers = "specs/property-wrappers/property-wrappers.spec.md"
swiftdata = "specs/swiftdata/swiftdata.spec.md"
1 change: 1 addition & 0 deletions .specsync/version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4.3.1
78 changes: 78 additions & 0 deletions Examples/DemoApp/Sources/AppStateDemoApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import SwiftUI

import DataDashboard
import MultiPlatformTracker
import SecureVault
import SettingsKit
import SyncNotes
import TodoCloud

#if canImport(SwiftData)
import SwiftDataExampleLib
#endif

// MARK: - App entry point

/// A host app that showcases every AppState example view on a real device or simulator.
///
/// Each row drives into the corresponding example's *public* root view, so what you see running
/// here is exactly the SwiftUI that the example packages ship and test.
@main
struct AppStateDemoApp: App {
var body: some Scene {
WindowGroup {
ExampleCatalogView()
}
}
}

// MARK: - Catalog

/// The list of examples, grouped the same way the repository organizes them.
@available(iOS 18.0, *)
struct ExampleCatalogView: View {
var body: some View {
NavigationStack {
List {
Section("Moderate") {
NavigationLink("TodoCloud — @SyncState") {
TodoListView()
}
NavigationLink("SettingsKit — @StoredState + @Slice") {
SettingsView()
}
NavigationLink("DataDashboard — Dependency injection") {
DataDashboard.DashboardView()
}
NavigationLink("SecureVault — @SecureState") {
VaultView()
}
}

Section("Focused") {
NavigationLink("SyncNotes — @SyncState") {
NotesView()
}
NavigationLink("MultiPlatformTracker — @StoredState") {
TrackerView()
}
}

#if canImport(SwiftData)
Section("SwiftData (3.0.0)") {
NavigationLink("SwiftData Lab — relationships, queries, migration") {
SwiftDataLabView()
}
}
#endif

Section("Stress") {
NavigationLink("Break It — try to crash AppState") {
BreakItView()
}
}
}
.navigationTitle("AppState 3.0.0")
}
}
}
130 changes: 130 additions & 0 deletions Examples/DemoApp/Sources/BreakItView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import AppState
import SwiftUI

#if canImport(SwiftData)
import SwiftDataExampleLib
#endif

// MARK: - Break-It stress state

extension Application {
/// A counter hammered by the stress harness.
fileprivate var stressCounter: State<Int> {
state(initial: 0)
}

/// A `UserDefaults`-backed array grown to large sizes by the stress harness.
fileprivate var stressLog: StoredState<[Int]> {
storedState(initial: [], id: "breakIt.stressLog")
}
}

// MARK: - BreakItView

/// An interactive "try to crash it" screen.
///
/// Every button runs an abusive workload against AppState — tight mutation loops, large persisted
/// arrays, mass SwiftData inserts, rapid `reset` churn, and concurrent off-main writes — and reports
/// how long it took and that the app is still standing. The point is to *watch it survive* on a real
/// device or simulator.
@available(iOS 18.0, *)
struct BreakItView: View {

// MARK: - State

@AppState(\.stressCounter) private var counter: Int
@StoredState(\.stressLog) private var log: [Int]

@State private var lastResult: String = "Tap a button to try to break AppState."
@State private var isRunning: Bool = false

// MARK: - Body

var body: some View {
List {
Section {
Text(lastResult)
.font(.callout.monospaced())
LabeledContent("counter", value: "\(counter)")
LabeledContent("stored array", value: "\(log.count) items")
} header: {
Text("Status")
}

Section {
stressButton("Hammer @AppState ×100k") {
for _ in 0..<100_000 { counter &+= 1 }
return "counter survived 100k writes → \(counter)"
}
stressButton("Grow @StoredState to 20k") {
log = Array(0..<20_000)
return "UserDefaults-backed array → \(log.count) items"
}
stressButton("Rapid reset churn ×5k") {
for _ in 0..<5_000 {
Application.reset(\.stressCounter)
}
return "survived 5k resets; counter = \(counter)"
}
stressButton("Concurrent off-main writes ×10k") {
DispatchQueue.concurrentPerform(iterations: 10_000) { index in
_ = Application.dependency(\.logger)
_ = index
}
return "10k concurrent dependency reads, no crash"
}
#if canImport(SwiftData)
stressButton("Mass SwiftData insert ×2k") {
let store = TodoListStore()
store.createList(titled: "Stress \(counter)")
guard let list = store.lists.last else {
return "no list created"
}
let items = TodoItemStore(list: list)
for index in 0..<2_000 {
items.addItem(titled: "Item \(index)", priority: index % 5)
}
let total = Application.modelState(\.allItems).models.count
return "inserted 2k SwiftData items → \(total) total"
}
stressButton("Cascade-delete everything") {
let store = TodoListStore()
for list in store.lists {
store.delete(list)
}
let remaining = Application.modelState(\.allItems).models.count
return "cascade-deleted all lists → \(remaining) items remain"
}
#endif
} header: {
Text("Abusive workloads")
} footer: {
Text("Each runs synchronously on the main actor, then reports elapsed time. If the app is still responsive afterwards, AppState held up.")
}

Section {
Button("Reset everything", role: .destructive) {
Application.reset(\.stressCounter)
log = []
lastResult = "Reset."
}
}
}
.navigationTitle("Break It")
.disabled(isRunning)
}

// MARK: - Helpers

private func stressButton(_ title: String, _ work: @escaping () -> String) -> some View {
Button(title) {
isRunning = true
let clock = ContinuousClock()
var summary = ""
let elapsed = clock.measure { summary = work() }
let millis = Double(elapsed.components.attoseconds) / 1_000_000_000_000_000 + Double(elapsed.components.seconds) * 1_000
lastResult = "✓ \(summary)\n (\(String(format: "%.1f", millis)) ms)"
isRunning = false
}
}
}
Loading
Loading