Skip to content

3.0.0#148

Merged
0xLeif merged 33 commits into
mainfrom
develop
Jun 11, 2026
Merged

3.0.0#148
0xLeif merged 33 commits into
mainfrom
develop

Conversation

@0xLeif

@0xLeif 0xLeif commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

AppState 3.0.0 — modernizes the library for the Observation framework and adds first-class SwiftData support, plus an extensive example + test buildout.

Library

  • Observation migrationApplication is now @Observable; reads register via registerObservation() and writes bump an anchor via notifyChange().
  • SwiftDataModelState / @ModelState, a ModelContainer dependency, and modelContext/modelState accessors.
  • Bug fixes from an adversarial test pass:
    • SyncState now encodes before committing to the local fallback (a non-encodable value no longer poisons the fallback).
    • Keychain serializes set()'s update/add under the lock, updates its in-memory key index synchronously, and remove() now drops the key from the index.
    • Application.state(_:).value (imperative accessor) now registers observation like the property wrappers.

Examples

  • Six SwiftUI example packages (TodoCloud, SettingsKit, DataDashboard, SecureVault, SyncNotes, MultiPlatformTracker), each with a passing test suite (6 at literal 100% coverage).
  • SwiftDataExample expanded into a multi-model SwiftData Lab — cascade/nullify relationships, compound queries, @Attribute(.unique) upsert, and a VersionedSchema V1→V2 migration.
  • A runnable DemoApp (xcodegen) cataloging every example, with a SwiftData Lab screen and an interactive Break It stress screen.

Tests

  • Library: 199 unit + adversarial tests.
  • Examples: ~216 ViewInspector tests.
  • DemoApp: 10 XCUITests driving the real UI on the simulator.

Test Plan

  • swift test (library) — 199 passing
  • swift test in each example package — passing
  • xcodebuild test DemoApp XCUITests — 10 passing on iOS 26 simulator
  • DemoApp builds + runs on iPhone simulator

🤖 Generated with Claude Code

claude and others added 24 commits June 9, 2026 16:12
Introduce a SwiftData integration that follows AppState's existing
persistence-type conventions, fully gated behind `canImport(SwiftData)`
and `@available` (iOS 17 / macOS 14 / tvOS 17 / watchOS 10 / visionOS 1).

- ModelContainer as an AppState dependency, with `Application.modelContext(_:)`
  to access the shared main-actor `ModelContext` from non-view code, and a
  `modelContainer(_:)` registration convenience.
- `Application.ModelState<Model>` (conforms to `MutableApplicationState`):
  reads via a `FetchDescriptor`, with `insert`/`delete`/`save`/`reset` and a
  `value` get (fetch) / set (insert new + save) accessor.
- `@ModelState` property wrapper exposing `[Model]`, with the projected value
  surfacing `insert`/`delete`/`save`.
- Public factory/accessor helpers: `modelState(...)`, `Application.modelState(_:)`,
  and `Application.reset(modelState:)`.
- Unit tests (in-memory container) covering the dependency, CRUD through the
  application and property wrapper, reset, and fetch-descriptor ordering.
- Runnable example SwiftPM package (Examples/SwiftDataExample) wired into macOS
  CI via `swift build`/`swift run` as a smoke test.
- Documentation: usage-modelstate guide plus README and usage-overview updates.
Begin the v3 modernization. Raise the minimum deployment targets to
iOS 17 / watchOS 10 / macOS 14 / tvOS 17 / visionOS 1 and explicitly pin
`swiftLanguageModes: [.v6]`.

Because the new floors exactly match SwiftData's availability, the
`@available` annotations on the SwiftData integration are now redundant and
have been removed; the `#if canImport(SwiftData)` guards remain so Linux and
Windows (which have no SwiftData) still compile to nothing.
Add -Xswiftc -warnings-as-errors to the build and test steps on macOS,
Ubuntu, and Windows so any Swift 6 (or future-Swift) warning fails CI. This
establishes the zero-warning baseline for the v3 modernization.
…ature

Future-proof against upcoming Swift by enabling the ExistentialAny upcoming
feature on the AppState target and annotating every existential type with
'any' (the *Managing dependency protocols, Loggable, and Error). This keeps
the library warning-clean as ExistentialAny moves toward becoming the default.
Adopt @observable for `Application` while keeping `NSObject` (so the @objc
iCloud `didChangeExternally` hook is unchanged). State and dependency values
still live in the untracked `Cache`, so a private `changeAnchor` bridges cache
changes to Observation:

- `Application` is now `@Observable`; `lock`/`cache`/`bag` are `@ObservationIgnored`.
- `registerObservation()` (read by every property wrapper's getter) registers the
  current tracking scope; `notifyChange()` (public) bumps the anchor to update
  observers. The cache observer and `DependencySlice` setters call `notifyChange()`.
- Property wrappers (`AppState`, `Stored/File/Sync/Secure/ModelState`, `Slice`,
  `OptionalSlice`, `DependencySlice`) drop `@ObservedObject app` for a computed
  `app` and call `registerObservation()` when read. This also removes the
  per-wrapper Linux/Windows fork for the `app` reference.
- Removed `Application: ObservableObject`; the enclosing-instance subscript that
  supports ObservableObject host view models is retained, as is `ObservedDependency`.

Note: reactive view updates require verification on a real Apple target; CI here
covers compilation, unit tests, and warnings-as-errors only.
- SecureState's setter wrote through the Keychain (not the cache), so it called
  the old objectWillChange; route it through notifyChange() instead.
- Drop the now-orphaned 'ObjectWillChangePublisher == ObservableObjectPublisher'
  constraint on consume(object:), which relied on Application's removed
  ObservableObject conformance.
Document the v3 breaking changes (raised platform floors, strict Swift 6 /
ExistentialAny, the Observation/@observable migration and notifyChange(), and
the additive SwiftData ModelState feature) and link it from the README.
…ages

Localize the 3.0 documentation into every language the repo carries
(de, es, fr, hi, pt, ru, zh-CN):

- Add translated `usage-modelstate.md` and `upgrade-to-v3.md` to each language.
- Add the ModelState section to each `usage-overview.md`.
- Update each `usage-syncstate.md` custom-Application example to call
  `notifyChange()` instead of `objectWillChange.send()` (including English).
- Update each translated README's requirements to the new platform floors and
  add the SwiftData (ModelState) feature bullet plus the two new doc links.
Use withObservationTracking to assert that reading a property wrapper registers
an observation dependency and that mutating the underlying state fires onChange
(the same registerObservation()/notifyChange() path SwiftUI relies on), plus a
negative case. Apple-only, since the cache->anchor bridge is Apple-only.
…pecs)

Integrate CorvidLabs fledge and spec-sync into the project:

- fledge.toml: build/test/lint/example tasks; the lint task runs build+test with
  -warnings-as-errors and the example smoke test, mirroring CI.
- .specsync/ (v4.3.1): config, registry (application, property-wrappers,
  swiftdata), version.
- specs/application: updated to v3 (Observation/@observable, notifyChange,
  raised floors, Swift 6 + ExistentialAny) plus filled context/requirements/
  tasks/testing.
- specs/swiftdata: new module documenting the SwiftData ModelContainer
  dependency and ModelState.

(property-wrappers spec follows in a separate commit.)
Fill the property-wrappers spec and its context/requirements/tasks/testing:
all state, dependency, and slice/constant wrappers; the Observation-based
reactivity (computed app + registerObservation/notifyChange); @ModelState;
and the DynamicProperty + enclosing-instance subscript behavior.
- Use the unqualified log(...) inside the nested Application.ModelState struct,
  matching FileState/StoredState (the qualified Application.log is kept in the
  top-level @ModelState wrapper, consistent with the other wrappers).
- Correct the @DependencySlice 'app' doc comment to reference a dependency.
Act on the maintainer review of the SwiftData / v3 work:

- ModelState semantics (#7/#8/#9): `value` is now a read-only `models` (no more
  insert-only "set"); the destructive `reset()` is an explicit `deleteAll()` and
  `ModelState` no longer conforms to `MutableApplicationState` (so it isn't forced
  into value/reset semantics that don't fit). Removed `Application.reset(modelState:)`.
  Documented loudly that `models` performs a live fetch on every read.
- `@ModelState` wrapper is read-only; mutate through the projected value. Dropped
  the now-unneeded enclosing-instance subscript and the Combine import.
- Observation isolation (#4/#5): `changeAnchor`, `registerObservation()`, and
  `notifyChange()` are `@MainActor`; the cache observer routes through
  `MainActor.assumeIsolated`. Documented why the discarded anchor read is load-bearing.
- Scoped strict builds: `-warnings-as-errors` moved out of the CI command and into
  env-gated `unsafeFlags` on our targets only (APPSTATE_STRICT), so dependencies and
  downstream consumers are unaffected.
- Docs: replaced `try!` ModelContainer examples with explicit do/catch.
- Updated tests and the example for the new API.
The @mainactor isolation attempted for the anchor is incompatible with the
cross-thread Combine cache observer (a nonisolated sink cannot hop to a
@mainactor method without 'sending' non-Sendable self — Swift 6 region
isolation error on macOS). Removing the central observer instead would risk
silently dropping reactivity for some state types, which CI can't detect.

So notifyChange()/registerObservation()/changeAnchor stay nonisolated (the
proven, ObservationTests-covered design) and the thread-safety invariant is now
documented explicitly: all mutations funnel through notifyChange(), invoked
only from main-actor setters and the synchronous cache observer running during
them; reads are main-actor; the @observable registrar is itself synchronized.
Update usage-modelstate.md in all 8 languages and the SwiftData / property-wrappers
specs to the revised API: read-only `models` (live fetch per read) instead of a
read/write `value`; `deleteAll()` instead of `reset()`; `Application.reset(modelState:)`
removed; `@ModelState` wrapped value read-only (mutate via the projected value); and
`ModelContainer` examples use explicit do/catch instead of try!.
Document why v3 adopts @observable (modern, cross-platform Observation; drops the
NSObject/Combine ObservableObject coupling) and that the coarse, whole-registry
notification is unchanged from 2.x, with per-key observation noted as future work.
Include the trailing property-wrappers spec task update.
…egration-hsu92z

Add SwiftData integration (ModelContainer dependency + ModelState)
- ModelContainer: forward the autoclosure thunk to dependency(_:id:) instead
  of evaluating it eagerly, so the heavy ModelContainer is built once on first
  access instead of rebuilt on every \.modelContainer read (critical). Public
  @autoclosure signature is unchanged — non-breaking.
- notifyChange(): assert main-thread before mutating the @observable anchor.
  Application is non-Sendable so the change cannot be hopped to main on the
  caller's behalf; the invariant is enforced instead of silently raced.
- ModelState.deleteAll(): use context.delete(model:where:) batch deletion
  (DB-level, no objects loaded) instead of fetch-all-then-loop.
- SwiftDataExample: drop try! for a do/catch + fatalError container factory,
  matching the library's documented idiom and the no-force-try convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folds in standalone SwiftPM example apps demonstrating AppState, each with a
passing test suite (69 tests total), verified against the 3.0.0 root:

- Moderate/TodoCloud      — @syncstate + @AppDependency (18 tests)
- Moderate/SettingsKit    — @StoredState + @slice (14 tests)
- Moderate/DataDashboard  — dependency injection + @appstate (8 tests)
- Moderate/SecureVault    — @securestate / Keychain (11 tests)
- Focused/SyncNotes       — @syncstate (7 tests)
- Focused/MultiPlatformTracker — @StoredState cross-platform (11 tests)

Also adds .github/workflows/examples.yml (tests the six + builds the SwiftData
example) and ignores nested .build/ directories.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds ViewInspector (0.10.3) to each example's test target and exercises every
view body and action closure (tap, onSubmit, onDelete, setInput, isDisabled),
plus the live/non-mocked service implementations.

Coverage (regions/functions/lines), verified via llvm-cov:
- TodoCloud, SettingsKit, DataDashboard, SecureVault, SyncNotes,
  MultiPlatformTracker: 100% / 100% / 100%
- SwiftDataExample: 100% functions; the sole uncovered region is the defensive
  catch+fatalError in the in-memory ModelContainer factory — structurally
  uncoverable (SwiftData's init throws, try! is banned by convention, and
  executing the trap crashes the runner). Documented in-source.

Source simplifications made along the way (removed unreachable code):
- Dropped vestigial #available(watchOS 9.0) checks across examples (dead given
  the watchOS 11 deployment target).
- MultiPlatformTracker: simplified a constant-true ternary in decrement.
- Made nested view structs / shared test mocks internal for inspectability.
- SwiftDataExample: split into library + executable + test targets so the
  reusable types are testable; kept the clean do/catch+fatalError factory.

CI: examples.yml now runs Test Suite 'All tests' started at 2026-06-09 18:17:21.178.
Test Suite 'AppStatePackageTests.xctest' started at 2026-06-09 18:17:21.179.
Test Suite 'AppDependencyTests' started at 2026-06-09 18:17:21.179.
Test Case '-[AppStateTests.AppDependencyTests testComposableDependencies]' started.
Test Case '-[AppStateTests.AppDependencyTests testComposableDependencies]' passed (0.006 seconds).
Test Case '-[AppStateTests.AppDependencyTests testDependency]' started.
Test Case '-[AppStateTests.AppDependencyTests testDependency]' passed (0.002 seconds).
Test Suite 'AppDependencyTests' passed at 2026-06-09 18:17:21.187.
	 Executed 2 tests, with 0 failures (0 unexpected) in 0.008 (0.008) seconds
Test Suite 'AppStateTests' started at 2026-06-09 18:17:21.187.
Test Case '-[AppStateTests.AppStateTests testLoggingToggle]' started.
Test Case '-[AppStateTests.AppStateTests testLoggingToggle]' passed (0.002 seconds).
Test Case '-[AppStateTests.AppStateTests testPropertyWrappers]' started.
Test Case '-[AppStateTests.AppStateTests testPropertyWrappers]' passed (0.002 seconds).
Test Case '-[AppStateTests.AppStateTests testStateClosureCachesValueOnGet]' started.
Test Case '-[AppStateTests.AppStateTests testStateClosureCachesValueOnGet]' passed (0.002 seconds).
Test Case '-[AppStateTests.AppStateTests testState]' started.
Test Case '-[AppStateTests.AppStateTests testState]' passed (0.001 seconds).
Test Case '-[AppStateTests.AppStateTests testStateWithDifferentDataTypes]' started.
Test Case '-[AppStateTests.AppStateTests testStateWithDifferentDataTypes]' passed (0.002 seconds).
Test Suite 'AppStateTests' passed at 2026-06-09 18:17:21.198.
	 Executed 5 tests, with 0 failures (0 unexpected) in 0.010 (0.010) seconds
Test Suite 'ApplicationTests' started at 2026-06-09 18:17:21.198.
Test Case '-[AppStateTests.ApplicationTests testCustomFunction]' started.
Test Case '-[AppStateTests.ApplicationTests testCustomFunction]' passed (0.001 seconds).
Test Suite 'ApplicationTests' passed at 2026-06-09 18:17:21.199.
	 Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.001) seconds
Test Suite 'DependencySliceTests' started at 2026-06-09 18:17:21.199.
Test Case '-[AppStateTests.DependencySliceTests testApplicationSliceFunction]' started.
Test Case '-[AppStateTests.DependencySliceTests testApplicationSliceFunction]' passed (0.002 seconds).
Test Case '-[AppStateTests.DependencySliceTests testPropertyWrappers]' started.
Test Case '-[AppStateTests.DependencySliceTests testPropertyWrappers]' passed (0.002 seconds).
Test Suite 'DependencySliceTests' passed at 2026-06-09 18:17:21.203.
	 Executed 2 tests, with 0 failures (0 unexpected) in 0.004 (0.004) seconds
Test Suite 'FileManagerExtensionTests' started at 2026-06-09 18:17:21.203.
Test Case '-[AppStateTests.FileManagerExtensionTests testWriteAndReadCodable]' started.
Test Case '-[AppStateTests.FileManagerExtensionTests testWriteAndReadCodable]' passed (0.003 seconds).
Test Case '-[AppStateTests.FileManagerExtensionTests testWriteAndReadData]' started.
Test Case '-[AppStateTests.FileManagerExtensionTests testWriteAndReadData]' passed (0.001 seconds).
Test Suite 'FileManagerExtensionTests' passed at 2026-06-09 18:17:21.207.
	 Executed 2 tests, with 0 failures (0 unexpected) in 0.004 (0.004) seconds
Test Suite 'FileStateTests' started at 2026-06-09 18:17:21.207.
Test Case '-[AppStateTests.FileStateTests testFileState]' started.
Test Case '-[AppStateTests.FileStateTests testFileState]' passed (0.013 seconds).
Test Case '-[AppStateTests.FileStateTests testStoringViewModel]' started.
Test Case '-[AppStateTests.FileStateTests testStoringViewModel]' passed (0.008 seconds).
Test Suite 'FileStateTests' passed at 2026-06-09 18:17:21.228.
	 Executed 2 tests, with 0 failures (0 unexpected) in 0.020 (0.020) seconds
Test Suite 'KeychainTests' started at 2026-06-09 18:17:21.228.
Test Case '-[AppStateTests.KeychainTests testKeychainContains]' started.
Test Case '-[AppStateTests.KeychainTests testKeychainContains]' passed (0.045 seconds).
Test Case '-[AppStateTests.KeychainTests testKeychainInitKeys]' started.
Test Case '-[AppStateTests.KeychainTests testKeychainInitKeys]' passed (0.002 seconds).
Test Case '-[AppStateTests.KeychainTests testKeychainInitValues]' started.
Test Case '-[AppStateTests.KeychainTests testKeychainInitValues]' passed (0.033 seconds).
Test Case '-[AppStateTests.KeychainTests testKeychainRequiresFailure]' started.
Test Case '-[AppStateTests.KeychainTests testKeychainRequiresFailure]' passed (0.002 seconds).
Test Case '-[AppStateTests.KeychainTests testKeychainRequiresSuccess]' started.
Test Case '-[AppStateTests.KeychainTests testKeychainRequiresSuccess]' passed (0.025 seconds).
Test Case '-[AppStateTests.KeychainTests testKeychainValues]' started.
Test Case '-[AppStateTests.KeychainTests testKeychainValues]' passed (0.030 seconds).
Test Suite 'KeychainTests' passed at 2026-06-09 18:17:21.364.
	 Executed 6 tests, with 0 failures (0 unexpected) in 0.136 (0.137) seconds
Test Suite 'ModelStateTests' started at 2026-06-09 18:17:21.364.
Test Case '-[AppStateTests.ModelStateTests testDeleteAll]' started.
Test Case '-[AppStateTests.ModelStateTests testDeleteAll]' passed (0.011 seconds).
Test Case '-[AppStateTests.ModelStateTests testFetchDescriptorSorting]' started.
Test Case '-[AppStateTests.ModelStateTests testFetchDescriptorSorting]' passed (0.004 seconds).
Test Case '-[AppStateTests.ModelStateTests testInsertAndFetchThroughApplication]' started.
Test Case '-[AppStateTests.ModelStateTests testInsertAndFetchThroughApplication]' passed (0.002 seconds).
Test Case '-[AppStateTests.ModelStateTests testModelContextDependency]' started.
Test Case '-[AppStateTests.ModelStateTests testModelContextDependency]' passed (0.002 seconds).
Test Case '-[AppStateTests.ModelStateTests testProjectedValueCRUD]' started.
Test Case '-[AppStateTests.ModelStateTests testProjectedValueCRUD]' passed (0.005 seconds).
Test Case '-[AppStateTests.ModelStateTests testPropertyWrapperReadAndProjectedInsert]' started.
Test Case '-[AppStateTests.ModelStateTests testPropertyWrapperReadAndProjectedInsert]' passed (0.005 seconds).
Test Suite 'ModelStateTests' passed at 2026-06-09 18:17:21.393.
	 Executed 6 tests, with 0 failures (0 unexpected) in 0.028 (0.029) seconds
Test Suite 'ObservationTests' started at 2026-06-09 18:17:21.393.
Test Case '-[AppStateTests.ObservationTests testMutatingStateNotifiesObservers]' started.
Test Case '-[AppStateTests.ObservationTests testMutatingStateNotifiesObservers]' passed (0.001 seconds).
Test Case '-[AppStateTests.ObservationTests testReadingWithoutTrackedMutationDoesNotNotify]' started.
Test Case '-[AppStateTests.ObservationTests testReadingWithoutTrackedMutationDoesNotNotify]' passed (0.001 seconds).
Test Suite 'ObservationTests' passed at 2026-06-09 18:17:21.395.
	 Executed 2 tests, with 0 failures (0 unexpected) in 0.001 (0.002) seconds
Test Suite 'ObservedDependencyTests' started at 2026-06-09 18:17:21.395.
Test Case '-[AppStateTests.ObservedDependencyTests testDependency]' started.
Test Case '-[AppStateTests.ObservedDependencyTests testDependency]' passed (0.001 seconds).
Test Suite 'ObservedDependencyTests' passed at 2026-06-09 18:17:21.396.
	 Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.001) seconds
Test Suite 'OptionalSliceTests' started at 2026-06-09 18:17:21.396.
Test Case '-[AppStateTests.OptionalSliceTests testApplicationSliceFunction]' started.
Test Case '-[AppStateTests.OptionalSliceTests testApplicationSliceFunction]' passed (0.002 seconds).
Test Case '-[AppStateTests.OptionalSliceTests testNil]' started.
Test Case '-[AppStateTests.OptionalSliceTests testNil]' passed (0.002 seconds).
Test Case '-[AppStateTests.OptionalSliceTests testPropertyWrappers]' started.
Test Case '-[AppStateTests.OptionalSliceTests testPropertyWrappers]' passed (0.002 seconds).
Test Suite 'OptionalSliceTests' passed at 2026-06-09 18:17:21.402.
	 Executed 3 tests, with 0 failures (0 unexpected) in 0.005 (0.005) seconds
Test Suite 'SecureStateTests' started at 2026-06-09 18:17:21.402.
Test Case '-[AppStateTests.SecureStateTests testSecureState]' started.
Test Case '-[AppStateTests.SecureStateTests testSecureState]' passed (0.048 seconds).
Test Case '-[AppStateTests.SecureStateTests testStoringViewModel]' started.
Test Case '-[AppStateTests.SecureStateTests testStoringViewModel]' passed (0.041 seconds).
Test Suite 'SecureStateTests' passed at 2026-06-09 18:17:21.491.
	 Executed 2 tests, with 0 failures (0 unexpected) in 0.089 (0.089) seconds
Test Suite 'SliceTests' started at 2026-06-09 18:17:21.491.
Test Case '-[AppStateTests.SliceTests testApplicationSliceFunction]' started.
Test Case '-[AppStateTests.SliceTests testApplicationSliceFunction]' passed (0.002 seconds).
Test Case '-[AppStateTests.SliceTests testPropertyWrappers]' started.
Test Case '-[AppStateTests.SliceTests testPropertyWrappers]' passed (0.002 seconds).
Test Suite 'SliceTests' passed at 2026-06-09 18:17:21.495.
	 Executed 2 tests, with 0 failures (0 unexpected) in 0.004 (0.004) seconds
Test Suite 'StoredStateTests' started at 2026-06-09 18:17:21.495.
Test Case '-[AppStateTests.StoredStateTests testStoredState]' started.
Test Case '-[AppStateTests.StoredStateTests testStoredState]' passed (0.002 seconds).
Test Case '-[AppStateTests.StoredStateTests testStoringViewModel]' started.
Test Case '-[AppStateTests.StoredStateTests testStoringViewModel]' passed (0.003 seconds).
Test Suite 'StoredStateTests' passed at 2026-06-09 18:17:21.500.
	 Executed 2 tests, with 0 failures (0 unexpected) in 0.005 (0.006) seconds
Test Suite 'SyncStateTests' started at 2026-06-09 18:17:21.500.
Test Case '-[AppStateTests.SyncStateTests testFailEncodingSyncState]' started.
Test Case '-[AppStateTests.SyncStateTests testFailEncodingSyncState]' passed (0.007 seconds).
Test Case '-[AppStateTests.SyncStateTests testStoringViewModel]' started.
Test Case '-[AppStateTests.SyncStateTests testStoringViewModel]' passed (0.004 seconds).
Test Case '-[AppStateTests.SyncStateTests testSyncState]' started.
Test Case '-[AppStateTests.SyncStateTests testSyncState]' passed (0.003 seconds).
Test Suite 'SyncStateTests' passed at 2026-06-09 18:17:21.514.
	 Executed 3 tests, with 0 failures (0 unexpected) in 0.014 (0.014) seconds
Test Suite 'AppStatePackageTests.xctest' passed at 2026-06-09 18:17:21.515.
	 Executed 41 tests, with 0 failures (0 unexpected) in 0.330 (0.335) seconds
Test Suite 'All tests' passed at 2026-06-09 18:17:21.515.
	 Executed 41 tests, with 0 failures (0 unexpected) in 0.330 (0.337) seconds
◇ Test run started.
↳ Testing Library Version: 1902
↳ Target Platform: arm64e-apple-macos14.0
✔ Test run with 0 tests in 0 suites passed after 0.001 seconds. for SwiftDataExample (was build-only).

257 tests pass across the library (41) and seven examples (216).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds ViewInspector to the library test target and +97 tests for the new 3.0.0
surface (138 total, all passing):

- ModelStateCoverageTests: every modelState/modelContainer/modelContext overload,
  ModelState CRUD edge cases (save no-op, delete missing, deleteAll empty),
  multi-container isolation, feature/id scoping, FetchDescriptor sort/predicate/
  limit, and @ModelState from struct + class. Functions 100% on the SwiftData files.
- PropertyWrapperViewTests: every property wrapper (@appstate, @StoredState,
  @syncstate, @securestate, @FileState, @Slice/@OptionalSlice, @Constant/
  @OptionalConstant, @AppDependency, @ObservedDependency, @ModelState) rendered
  and driven inside a real SwiftUI view via ViewInspector.
- ObservationBridgeTests: registerObservation()/notifyChange() fires for each
  state wrapper, multiple observers, direct notifyChange(), and didChangeExternally().

Coverage: library 86.3%/92.5% -> 88.7%/94.1% (regions/lines); Application.swift
75%->96.7% lines; the SwiftData ModelState/ModelContainer files at 100% functions.

Remaining uncovered are structurally uncoverable: the three SwiftData catch
blocks (in-memory SwiftData raises uncatchable NSExceptions, not Swift errors),
the notifyChange() assert-false branch, and Linux-only #else branches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…suite

- Examples/DemoApp: an xcodegen-generated iOS host app cataloging every example
  (verified building + running on the iOS 17 simulator), with a SwiftData Lab
  section and an interactive 'Break It' stress screen that hammers AppState live.
- Examples/SwiftDataExample: expanded into a multi-model SwiftData Lab —
  TodoList/TodoItem/Tag with cascade + nullify @relationships, compound #Predicate
  queries with multi-sort/fetchLimit, @Attribute(.unique) upsert, a VersionedSchema
  V1->V2 + SchemaMigrationPlan, and SwiftUI views (SwiftDataLabView). 81 tests.
- Tests/AppStateTests/AdversarialBreakItTests: 58 'try to break it' tests across
  concurrency/volume/churn/malformed-data/SwiftData-edge/re-entrancy. Library suite
  196 tests, all passing. These surfaced real findings (see follow-up): a SyncState
  encode-ordering bug, Keychain set()/values() races, and an observation gap on the
  non-wrapper Application.state().value accessor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- SyncState: encode the value BEFORE committing to the local fallback and iCloud.
  Previously storedState was written first, so a value that failed to JSON-encode
  (e.g. Double.infinity) poisoned the fallback and was read back even though iCloud
  never received it. A pre-existing test asserted that buggy behavior — updated.
- Keychain: serialize set()'s SecItemUpdate/SecItemAdd pair under the lock (two
  concurrent same-key writes could both attempt SecItemAdd), update the in-memory
  key index synchronously inside the lock (was a fire-and-forget Task that lagged),
  and have remove() drop the key from the index (it never did, so values() kept
  reporting removed keys). Keychain is now @unchecked Sendable with all mutable
  state lock-guarded.
- Observation: Application.state(_:).value (the imperative accessor) now registers
  the observation scope like the @appstate wrapper does, so reads through it drive
  withObservationTracking/SwiftUI updates.

Adds BugFixRegressionTests pinning each fix. Library suite: 199 tests, all passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
End-to-end UI tests (Examples/DemoApp/UITests) that launch the app and drive the
real SwiftUI on a simulator — 10 tests, all passing on iOS 26 (iPhone 16e):
- catalog lists every example; every screen is reachable and returns cleanly
- TodoCloud add todo, SettingsKit toggle, SyncNotes add note, Tracker increment,
  SecureVault login/logout, DataDashboard async load, SwiftData Lab create list,
  Break It stress workload survives

Adds an AppStateDemoUITests ui-testing target + scheme to project.yml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modernizes the AppState library to version 3.0.0 by raising the platform requirements, adopting Apple's modern Observation framework (@observable), and introducing a new ModelState component for seamless SwiftData integration. The code review highlighted two critical issues that need to be addressed: first, database errors are currently swallowed within ModelState's CRUD operations rather than being propagated to the caller; second, observing dependencies that publish changes on background threads can trigger notifyChange() off the main thread, violating its main-thread assertion and causing crashes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread Sources/AppState/Application/Application.swift
0xLeif and others added 5 commits June 9, 2026 20:33
- SwiftDataExample: a @Modelactor BulkImporter that inserts thousands of models on
  a background context (off the main actor), batching + yielding, with progress and
  cancellation; plus a responsive BulkImportView (ProgressView + Cancel) that stays
  interactive while 10k items import. The main actor only updates small progress
  state. 113 tests.
- DemoApp Break It: rewrote every workload to run in a yielding Task (or off-main
  via Task.detached + concurrentPerform) so the heavy loops never block the run
  loop — the spinner animates and the list scrolls during the work. Fixes the
  earlier main-thread freeze.
- DemoApp catalog: added the Bulk Import screen under SwiftData.
- XCUITests: added testBulkImportRunsOffMainAndCompletes (Cancel is hittable while
  the import runs off-main = proof the UI is not blocked); updated Break It +
  reachability tests. 12 UI tests passing on iOS 26 simulator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Library: make the ViewInspector test dependency Apple-only (.when(platforms:)) —
  it imports SwiftUI, which doesn't exist on Linux/Windows, breaking those builds.
  The one test file using it is already #if-guarded off those platforms.
- SwiftDataExample: mark the VersionedSchema static constants nonisolated(unsafe)
  (Schema.Version / MigrationStage aren't Sendable on older SDKs, so strict
  concurrency rejected them on CI's Xcode 16.x while passing on newer local SDKs).
- CI: bump the macOS/examples workflows to xcode-version: latest-stable so the SDK
  is recent enough for ViewInspector 0.10.x (AttributedTextSelection, Map types).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- AdversarialBreakItTests: scope the whole file to !os(Linux) && !os(Windows) and
  add 'import Observation'. It exercises Apple-only surface (Keychain, SecureState,
  SwiftData, Observation), so it broke the Linux/Windows library builds.
- Workflows: drop swift-actions/setup-swift and use the Xcode latest-stable bundled
  Swift instead. The standalone 6.1/6.2 toolchain mismatched latest-stable's 6.2.3
  SDK ('failed to build module Foundation/Combine/Darwin'); Xcode's bundled compiler
  matches its SDK and already parses the tools-version 6.2 example manifests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the NSLock + @mainactor mutable key index with:
- writeLock (NSLock) — serializes Keychain WRITE syscalls (set/remove) so the
  update-then-add sequence stays atomic.
- index: OSAllocatedUnfairLock<Set<Key>> — guards the in-memory key index in short
  critical sections that never span a syscall.

Both stored properties are immutable lets and Sendable, so Keychain conforms to
Sendable without @unchecked. Reads are now lock-free (a single system-serialized
syscall), so concurrent gets no longer block each other.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review: AppState stays a library — runnable demos/examples live in a separate
repo, and a third-party SwiftUI-inspection test dependency does not belong in the
manifest.

- Remove the entire Examples/ tree (6 example packages, the SwiftData Lab, the
  DemoApp + XCUITests) and the examples CI workflow.
- Drop the ViewInspector package dependency from Package.swift and delete the one
  test that used it (PropertyWrapperViewTests). The Observation bridge is already
  covered dependency-free by ObservationTests/ObservationBridgeTests via
  withObservationTracking.
- macOS.yml: drop the now-removed SwiftData example build/run steps.

Library: 161 tests, all passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0xLeif and others added 4 commits June 10, 2026 19:07
- Observation: the @appstate wrapper no longer calls registerObservation() itself —
  Application.state(_:) already registers the Observation scope, so reading through
  it is the single path. Reading Application.state(_:).value directly now also
  participates in observation, which is how non-SwiftUI code (any object using
  withObservationTracking) can observe AppState. Documented in upgrade-to-v3.
- Docs: tightened the 3.0 docs to match the project's voice and removed AI-shaped
  phrasing — upgrade-to-v3 (skimmable breaking-change list), usage-modelstate
  (example-first rewrite), usage-overview (dropped a try! from a snippet), README.

Library: 161 tests, all passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…0 docs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pinned setup-swift 6.1 + Xcode 16.0 caused the same SDK/compiler
mismatch we removed from the other workflows, and 16.0 lacks the iOS 17
SwiftData SDK the 3.0 target needs. Align with macOS.yml so the API
reference publishes when develop merges to main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…endency notify

Resolves both gemini high-priority comments on PR #148:

- ModelState: insert/delete/save/deleteAll kept lenient (log-and-swallow)
  but now back onto a shared throwing core, with a `strict` facade
  exposing throwing variants so callers can surface/recover from failed
  writes. Dual lenient/strict access matches the Keychain precedent.
- Application.consume(_:): a dependency that publishes objectWillChange
  off the main thread previously called notifyChange() off-main (debug
  assert / changeAnchor race). Now delivers synchronously when already on
  main (preserving the synchronous withObservationTracking contract) and
  hops to main only when off-main.

Adds ModelState strict-API regression tests and documents `strict` in the
ModelState guide across all 8 languages. Full suite: 163 tests, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@0xLeif 0xLeif merged commit 4325877 into main Jun 11, 2026
3 checks passed
@0xLeif 0xLeif deleted the develop branch June 11, 2026 04:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants