Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
/.build
Examples/SwiftDataExample/.build/
/Packages
xcuserdata/
DerivedData/
Expand Down
25 changes: 25 additions & 0 deletions Examples/SwiftDataExample/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// swift-tools-version: 6.0

import PackageDescription

let package = Package(
name: "SwiftDataExample",
platforms: [
.iOS(.v17),
.macOS(.v14),
.tvOS(.v17),
.watchOS(.v10),
.visionOS(.v1)
],
dependencies: [
.package(name: "AppState", path: "../..")
],
targets: [
.executableTarget(
name: "SwiftDataExample",
dependencies: [
.product(name: "AppState", package: "AppState")
]
)
]
)
86 changes: 86 additions & 0 deletions Examples/SwiftDataExample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# SwiftData + AppState Example

A small, self-contained SwiftPM executable that demonstrates AppState's SwiftData
integration. It shows how to register a SwiftData `ModelContainer` as an AppState
`Dependency`, expose a collection of `@Model` objects as an `Application.ModelState`,
and read/write that collection from both application-level call sites and the
`@ModelState` property wrapper.

## What it shows

- Registering an in-memory `ModelContainer` as an AppState dependency:
`Application.modelContainer`.
- Exposing a `ModelState<TodoItem>` collection: `Application.todos`.
- Inserting models two ways:
- the `@ModelState` projected value: `$todos.insert(...)`
- the application-level state: `Application.modelState(\.todos).insert(...)`
- Reading the models (`Application.modelState(\.todos).models`), updating + `save()`,
`delete(_:)`, and clearing everything with `Application.modelState(\.todos).deleteAll()`.
- Using `@ModelState` from a view-model-style `ObservableObject` (`TodoStore`).

Every step asserts the expected count with `precondition(...)`, so `swift run`
doubles as a smoke test. The example uses an in-memory store, so it is deterministic
and leaves nothing behind.

## Requirements

- macOS 14+ (SwiftData)
- Xcode 16+ / a Swift 6 toolchain

SwiftData only builds on Apple platforms, which is why this lives in a nested package
rather than the root `AppState` package.

## Running

```sh
cd Examples/SwiftDataExample
swift run
```

You should see the todos being inserted, updated, deleted, and finally reset, ending
with `== Example completed successfully ==` and a `0` exit code.

## Recommended reactive pattern for SwiftUI

`@ModelState` is intended for view models, services, and other non-view code that
needs shared, dependency-injected access to your models. Its mutations are **not**
automatically broadcast to SwiftUI. For reactive views, use SwiftData's own `@Query`
while sharing the AppState-provided `ModelContainer`:

```swift
import AppState
import SwiftData
import SwiftUI

@main
struct TodoApp: App {
var body: some Scene {
WindowGroup {
TodoListView()
}
// Share the same container AppState manages, so @Query and @ModelState
// read and write through one source of truth.
.modelContainer(Application.dependency(\.modelContainer))
}
}

struct TodoListView: View {
// @Query drives the reactive view.
@Query private var todos: [TodoItem]

// A view model using @ModelState for shared, non-view logic.
@StateObject private var store = TodoStore()

var body: some View {
List(todos) { todo in
Text(todo.title)
}
.toolbar {
Button("Add") { store.add("New todo") }
}
}
}
```

In short: use `@Query` for reactive views, and `@ModelState` (or
`Application.modelState(_:)`) for view models and services.
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import AppState
import Foundation

#if canImport(SwiftData)
import SwiftData

// MARK: - Model

/// A simple SwiftData model persisted through an AppState-provided `ModelContainer`.
///
/// The package's deployment target is macOS 14 / iOS 17 (see `Package.swift`), so no `@available`
/// annotations are needed here — SwiftData is unconditionally available.
@Model
final class TodoItem {
var title: String
var isDone: Bool

init(title: String, isDone: Bool = false) {
self.title = title
self.isDone = isDone
}
}

// MARK: - AppState wiring

extension Application {
/// An in-memory `ModelContainer` registered as an AppState dependency.
///
/// Using `isStoredInMemoryOnly: true` keeps the example deterministic and side-effect free,
/// so `swift run` can double as a smoke test in CI.
var modelContainer: Dependency<ModelContainer> {
modelContainer(
try! ModelContainer(
for: TodoItem.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
)
}

/// The shared collection of `TodoItem`s, backed by the `modelContainer` dependency.
var todos: ModelState<TodoItem> {
modelState(container: \.modelContainer)
}
}

// MARK: - View model / service usage

/// Demonstrates the `@ModelState` property wrapper from a view-model-style `ObservableObject`.
///
/// `@ModelState` is intended for view models, services, and other non-view code that needs
/// shared, dependency-injected access to your models. For reactive SwiftUI views, prefer
/// SwiftData's own `@Query` while sharing this same `ModelContainer` (see the README).
@MainActor
final class TodoStore: ObservableObject {
@ModelState(\.todos) var todos: [TodoItem]

/// Adds a todo via the projected value's explicit `insert(_:)`.
func add(_ title: String) {
$todos.insert(TodoItem(title: title))
}

/// Persists any pending changes via the projected value's `save()`.
func save() {
$todos.save()
}
}

// MARK: - Entry point

@main
struct SwiftDataExample {
// `main()` is `@MainActor` because the backing `ModelContainer.mainContext` (and therefore
// every `ModelState` operation) is main-actor bound.
@MainActor
static func main() {
// Surface AppState's internal logging so the run is easy to follow.
Application.logging(isEnabled: true)

print("== SwiftData + AppState example ==")

// Start from a clean slate so repeated runs are deterministic.
Application.modelState(\.todos).deleteAll()
precondition(Application.modelState(\.todos).models.isEmpty, "Expected an empty store at start")

// 1. Insert via the property-wrapper projected value (view-model style).
let store = TodoStore()
store.add("Buy milk")
print("After store.add: \(store.todos.count) todo(s)")
precondition(store.todos.count == 1, "Expected 1 todo after store.add")

// 2. Insert more through the view model (its projected-value `insert`).
store.add("Walk the dog")
store.add("Write code")
print("After two more inserts: \(store.todos.count) todo(s)")
precondition(store.todos.count == 3, "Expected 3 todos")

// 3. Insert directly through the application-level `ModelState`.
Application.modelState(\.todos).insert(TodoItem(title: "Read a book"))
print("After Application.modelState insert: \(Application.modelState(\.todos).models.count) todo(s)")
precondition(Application.modelState(\.todos).models.count == 4, "Expected 4 todos")

// Fetch & print the current todos.
let current = Application.modelState(\.todos).models
print("Current todos:")
for todo in current {
print(" - [\(todo.isDone ? "x" : " ")] \(todo.title)")
}

// 4. Mark one todo done and persist the change.
if let first = current.first {
first.isDone = true
Application.modelState(\.todos).save()
print("Marked \"\(first.title)\" as done and saved")
}
let doneCount = Application.modelState(\.todos).models.filter(\.isDone).count
precondition(doneCount == 1, "Expected exactly 1 completed todo")

// 5. Delete one todo.
if let toDelete = Application.modelState(\.todos).models.last {
Application.modelState(\.todos).delete(toDelete)
print("Deleted \"\(toDelete.title)\"")
}
let remaining = Application.modelState(\.todos).models
print("Remaining todos:")
for todo in remaining {
print(" - [\(todo.isDone ? "x" : " ")] \(todo.title)")
}
precondition(remaining.count == 3, "Expected 3 todos after deletion")

// 6. deleteAll() removes every model managed by the state.
Application.modelState(\.todos).deleteAll()
precondition(Application.modelState(\.todos).models.isEmpty, "Expected an empty store after deleteAll")
print("Store cleared; \(Application.modelState(\.todos).models.count) todo(s) remaining")

print("== Example completed successfully ==")
exit(0)
}
}

#else

@main
struct SwiftDataExample {
static func main() {
print("SwiftData unavailable on this platform; nothing to demonstrate.")
}
}

#endif
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ Read this in other languages: [French](documentation/README.fr.md) | [German](do

> 🍎 Features marked with this symbol are specific to Apple platforms, as they rely on Apple technologies such as iCloud and the Keychain.

> Note: SwiftData support (`ModelState`) requires iOS 17.0+ / macOS 14.0+ / tvOS 17.0+ / watchOS 10.0+ / visionOS 1.0+. It is gated with `@available` and does not change AppState's base requirements.

## Key Features

**AppState** includes several powerful features to help manage state and dependencies:

- **State**: Centralized state management that allows you to encapsulate and broadcast changes across the app.
- **StoredState**: Persistent state using `UserDefaults`, ideal for saving small amounts of data between app launches.
- **FileState**: Persistent state stored using `FileManager`, useful for storing larger amounts of data securely on disk.
- 🍎 **SwiftData (ModelState)**: Manage SwiftData `@Model` objects through AppState by injecting a shared `ModelContainer` and reading models with `ModelState`.
- 🍎 **SyncState**: Synchronize state across multiple devices using iCloud, ensuring consistency in user preferences and settings.
- 🍎 **SecureState**: Store sensitive data securely using the Keychain, protecting user information such as tokens or passwords.
- **Dependency Management**: Inject dependencies like network services or database clients across your app for better modularity and testing.
Expand Down Expand Up @@ -85,6 +88,7 @@ Here’s a detailed breakdown of **AppState**'s documentation:
- [Slicing State](documentation/en/usage-slice.md): Access and modify specific parts of the state.
- [StoredState Usage Guide](documentation/en/usage-storedstate.md): How to persist lightweight data using `StoredState`.
- [FileState Usage Guide](documentation/en/usage-filestate.md): Learn how to persist larger amounts of data securely on disk.
- 🍎 [ModelState Usage Guide](documentation/en/usage-modelstate.md): Manage SwiftData `@Model` objects through a shared `ModelContainer`.
- [Keychain SecureState Usage](documentation/en/usage-securestate.md): Store sensitive data securely using the Keychain.
- [iCloud Syncing with SyncState](documentation/en/usage-syncstate.md): Keep state synchronized across devices using iCloud.
- [FAQ](documentation/en/faq.md): Answers to common questions when using **AppState**.
Expand Down
Loading