SwiftModel — composable `@Model` structs for SwiftUI

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.

3 Likes