3.0.0#147
Conversation
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 (0xLeif#7/0xLeif#8/0xLeif#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 (0xLeif#4/0xLeif#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)
There was a problem hiding this comment.
Code Review
This pull request modernizes the AppState library for Swift 6 and Apple's Observation framework, raising platform requirements and introducing first-class SwiftData integration via ModelState and the @ModelState property wrapper. Feedback on these changes highlights a critical performance issue where ModelContainer is eagerly instantiated on every access, a thread-safety and main-thread publication issue in notifyChange(), and opportunities to optimize deletion using SwiftData's batch deletion API.
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.
| func modelContainer( | ||
| _ container: @autoclosure () -> ModelContainer, | ||
| _ fileID: StaticString = #fileID, | ||
| _ function: StaticString = #function, | ||
| _ line: Int = #line, | ||
| _ column: Int = #column | ||
| ) -> Dependency<ModelContainer> { | ||
| dependency( | ||
| container(), | ||
| id: Application.codeID( | ||
| fileID: fileID, | ||
| function: function, | ||
| line: line, | ||
| column: column | ||
| ) | ||
| ) | ||
| } |
There was a problem hiding this comment.
Critical Performance Issue: Eager Instantiation of ModelContainer
In the current implementation, container is passed as an @autoclosure and evaluated immediately via container() on line 100.
Because modelContainer is defined as a computed property on Application extensions, every access to Application.shared.modelContainer (which happens on every model fetch, insert, delete, or save) evaluates this computed property and eagerly instantiates a new ModelContainer.
Creating a ModelContainer is an extremely heavy operation that sets up SQLite connections, performs migrations, and initializes the schema. Eagerly instantiating it on every access will cause severe performance degradation, high memory usage, and potential database locking or crashes.
Suggestion
Change the parameter to an @escaping closure and pass it lazily to the underlying dependency resolver so that the ModelContainer is only instantiated once when first resolved.
| func modelContainer( | |
| _ container: @autoclosure () -> ModelContainer, | |
| _ fileID: StaticString = #fileID, | |
| _ function: StaticString = #function, | |
| _ line: Int = #line, | |
| _ column: Int = #column | |
| ) -> Dependency<ModelContainer> { | |
| dependency( | |
| container(), | |
| id: Application.codeID( | |
| fileID: fileID, | |
| function: function, | |
| line: line, | |
| column: column | |
| ) | |
| ) | |
| } | |
| func modelContainer( | |
| _ container: @escaping () -> ModelContainer, | |
| _ fileID: StaticString = #fileID, | |
| _ function: StaticString = #function, | |
| _ line: Int = #line, | |
| _ column: Int = #column | |
| ) -> Dependency<ModelContainer> { | |
| dependency( | |
| container, | |
| id: Application.codeID( | |
| fileID: fileID, | |
| function: function, | |
| line: line, | |
| column: column | |
| ) | |
| ) | |
| } |
| public func notifyChange() { | ||
| changeAnchor &+= 1 | ||
| } |
There was a problem hiding this comment.
Thread-Safety & Main-Thread Publication Issue
The notifyChange() method mutates changeAnchor directly. Since Application is not actor-isolated and notifyChange() can be called from background threads (e.g., when reacting to background iCloud sync notifications or background Combine publishers), this mutation is susceptible to data races.
Furthermore, mutating an @Observable property on a background thread that is currently being tracked by a SwiftUI view will trigger runtime warnings:
Publishing changes from background threads is not supported; make sure to publish values from the Main Thread.
Suggestion
Ensure that changeAnchor is always mutated on the main thread. This guarantees thread-safety and prevents SwiftUI background thread publication warnings.
public func notifyChange() {
if Thread.isMainThread {
changeAnchor &+= 1
} else {
DispatchQueue.main.async {
self.changeAnchor &+= 1
}
}
}| var modelContainer: Dependency<ModelContainer> { | ||
| modelContainer( | ||
| try! ModelContainer( | ||
| for: TodoItem.self, | ||
| configurations: ModelConfiguration(isStoredInMemoryOnly: true) | ||
| ) | ||
| ) | ||
| } |
There was a problem hiding this comment.
Suggestion: Use Lazy Closure for ModelContainer
To align with the recommended lazy instantiation pattern for ModelContainer (preventing eager instantiation on every access), pass the initialization block as a closure.
| var modelContainer: Dependency<ModelContainer> { | |
| modelContainer( | |
| try! ModelContainer( | |
| for: TodoItem.self, | |
| configurations: ModelConfiguration(isStoredInMemoryOnly: true) | |
| ) | |
| ) | |
| } | |
| var modelContainer: Dependency<ModelContainer> { | |
| modelContainer { | |
| try! ModelContainer( | |
| for: TodoItem.self, | |
| configurations: ModelConfiguration(isStoredInMemoryOnly: true) | |
| ) | |
| } | |
| } |
| public func deleteAll() { | ||
| let context = context | ||
|
|
||
| do { | ||
| let models = try context.fetch(fetchDescriptor()) | ||
|
|
||
| for model in models { | ||
| context.delete(model) | ||
| } | ||
|
|
||
| save(context: context, action: "Deleting") | ||
| } catch { | ||
| log( |
There was a problem hiding this comment.
Performance Improvement: Use Batch Deletion
Currently, deleteAll() fetches all matching models into memory and deletes them one by one. For large datasets, this is highly inefficient, consumes significant memory, and can lead to out-of-memory crashes.
Since the minimum deployment targets for AppState 3.0 are iOS 17 and macOS 14, SwiftData's batch deletion API (context.delete(model:where:)) is unconditionally available. This performs a highly optimized database-level delete without loading any objects into memory.
Suggestion
Use context.delete(model:where:) with the predicate from the FetchDescriptor.
@MainActor
public func deleteAll() {
let context = context
do {
try context.delete(model: Model.self, where: fetchDescriptor().predicate)
save(context: context, action: "Deleting")
} catch {
log(
error: error,
message: "\(ModelState.emoji) ModelState Deleting",
fileID: #fileID,
function: #function,
line: #line,
column: #column
)
}
}|
Absorbed into the new
Review findings addressed (all four from the automated review):
Examples — added six standalone SwiftUI example packages (TodoCloud, SettingsKit, DataDashboard, SecureVault, SyncNotes, MultiPlatformTracker) with 69 passing tests, plus an Examples CI workflow. Library: builds clean, 41 tests pass. Closing in favor of |
* Add SwiftData integration (ModelContainer dependency + ModelState) 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. * Raise platform floors to iOS 17 / macOS 14 and pin Swift 6 language mode 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. * CI: treat warnings as errors to enforce strict Swift 6 cleanliness 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. * Adopt explicit existentials and enable the ExistentialAny upcoming feature 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. * Migrate reactivity from ObservableObject to the Observation framework 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. * Fix Observation migration leftovers on Apple platforms - 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. * Docs: add AppState 3.0 upgrade guide 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. * Docs: translate v3 guides and propagate notifyChange across all languages 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. * Add Observation reactivity test for the @observable bridge 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. * Add fledge task runner and spec-sync setup (application + swiftdata specs) 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.) * spec-sync: author the property-wrappers spec for v3 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. * Polish: align ModelState logging and doc wording with house style - 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. * Address review: ModelState API, observation isolation, scoped strict CI 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. * Keep observation anchor nonisolated; document the concurrency invariant 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. * Docs/specs: sync ModelState references to read-only models / deleteAll() 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!. * Docs: justify the @observable migration; final spec sync 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. * Fix: address PR #147 review findings - 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> * Add: six SwiftUI example packages + Examples CI 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> * Test: drive example SwiftUI to 100% coverage with ViewInspector 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> * Test: expand SwiftData + Observation (3.0.0) test coverage 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> * Add: runnable demo app, advanced SwiftData example, adversarial test 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> * Fix: correctness bugs surfaced by the adversarial suite - 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> * Test: add XCUITest UI suite for the demo app 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> * Add: non-blocking background SwiftData + responsive stress UI - 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> * CI: fix cross-platform + SDK-version build failures - 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> * CI: guard adversarial suite to Apple platforms; fix toolchain mismatch - 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> * Refactor: make Keychain checked-Sendable (drop @unchecked) 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> * Remove examples and ViewInspector to keep 3.0.0 a lean library 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> * Docs + observation: finalize 3.0 docs; single observation path for State - 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> * Docs: regenerate de/es/fr/hi/pt/ru/zh-CN translations to match the 3.0 docs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * CI: fix DocC Pages deploy to use latest-stable Xcode on macos-15 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> * Address #148 review: ModelState strict throwing API + main-thread dependency 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> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Corvid Agent <95454608+corvid-agent@users.noreply.github.com>
No description provided.