After about two and a half years using it day to day, I'm tagging SwiftModel 1.0.
The idea: write your models as plain structs with @Model, drive SwiftUI from them, and let the model layer handle the parts you'd normally wire by hand. Async work is tied to a model's lifetime, and each model knows its place in a hierarchy, which is how it reaches dependencies, events, environment and preferences by position rather than threading them through.
A whole search feature:
@Model struct SearchModel {
var query = ""
var results: [Repo] = []
func onActivate() {
node.task(id: query) { query in
results = (try? await node.gitHubClient.search(query)) ?? []
}
}
}
onActivate runs when the model goes live, node.task(id:) restarts the search when query changes, and it's all cancelled when the model leaves the hierarchy. No stored task, no teardown to remember. node.gitHubClient is a dependency you override in tests and previews.
The view is ordinary SwiftUI: @ObservedModel for updates, a binding for the query.
struct SearchView: View {
@ObservedModel var model: SearchModel
var body: some View {
List(model.results) { repo in Text(repo.name) }
.searchable(text: $model.query)
}
}
And the test falls out with no setup. Swap the dependency, poke the model, assert the outcome:
@Test(.modelTesting) func testSearch() async {
let model = SearchModel().withAnchor {
$0.gitHubClient.search = { _ in Repo.mocks }
}
model.query = "swift"
await expect(!model.results.isEmpty)
}
It's exhaustive: any unasserted state change, event, or still-running task fails the test, not just the final value. The model is thread-safe, so the test drives the real async work directly, and teardown is deterministic. And because models are structs, retain cycles can't form, so there's no [weak self] to forget.
Runs back to iOS 14. The README has the rest, plus a few example apps: https://github.com/bitofmind/swift-model
Spare-time project, so replies might take a day or two. Happy to answer anything.
Many of SwiftModel's ideas first appeared a few years ago in its predecessor, Swift One State. Macros opened up enough improvements that a successor was warranted. Both libraries grew out of real production needs: apps I've built and maintained for work.