Architecture
The Pragmatic MVVM pattern that swiftspawn-generated projects follow.
Generated projects follow a pattern called Pragmatic MVVM: MVVM optimized for solo developers shipping freelance apps in months, not enterprise teams shipping platforms over years.
The layers
Section titled “The layers”flowchart LR
V[View]
VM[ViewModel]
S[Service]
I[Infrastructure]
V -->|user actions: taps, text, gestures| VM
VM -->|observable state| V
VM -->|async calls| S
S -->|domain models / AppError| VM
S -->|APIRequest| I
I -->|Decodable / AppError| S
- View: SwiftUI, owns
@State viewModel, no business logic. - ViewModel:
@MainActor @Observable, owns state, calls services. - Service: one protocol + impl + mock per resource. Tests inject the mock.
- Infrastructure:
APIClient,Router,AppError,TokenStore,DIContainer. Deliberately minimal.
What each layer does
Section titled “What each layer does”A SwiftUI view holds a @State ViewModel and reads its observable state. It dispatches user input (taps, text, gestures) to the ViewModel. It does not call services, format errors, or contain business logic.
ViewModel
Section titled “ViewModel”A @MainActor @Observable class that owns the screen’s state, calls services, and exposes commands the view triggers. It does not import SwiftUI types beyond what’s necessary. Tests construct the ViewModel directly with a mock service.
Service
Section titled “Service”A trio: a protocol that declares what the resource can do, an implementation that talks to the API client, and a mock used in tests and previews. Services map raw network responses into domain models or AppError. They have no opinion about views.
Infrastructure
Section titled “Infrastructure”A small set of types every project shares:
APIClient: aURLSession-based client with an interceptor chain.APIRequest: the typed request shape services build.APIConfig: per-environment base URL, default headers, auth strategy.AppError: the single error type that flows through the app.Router: an@Observablethat owns the navigation path.Route: an enum the router pushes; each case maps to a screen.TokenStore: an actor that holds the auth token.DIContainer: the Factory container where services are registered.Log: a thin namespace overos.Logger.
What’s not here
Section titled “What’s not here”Notably absent: UseCases, Repositories, DataSources, Coordinators. They show up in enterprise codebases for good reasons, but for the kind of apps swiftspawn is built for, they tend to be indirection without payoff. Fewer layers, more leverage.
If you need a layer, add it: nothing prevents you from inserting a Repository between ViewModel and Service. The CLI just won’t generate one for you.
Dependency injection
Section titled “Dependency injection”Generated projects use Factory. Services are registered in DIContainer.swift between auto-managed markers, so swiftspawn generate service and the CRUD recipe can add new entries without touching your code.
A ViewModel pulls its dependencies via property wrappers:
@Injected(\.movieService) private var movieServiceIn tests you swap the implementation:
Container.shared.movieService.register { MockMovieService() }Routing
Section titled “Routing”Route is an enum with one case per destination. Router owns a NavigationPath. The root NavigationStack switches on the route to choose which view to render. swiftspawn generate screen and generate route add cases between markers in Route.swift and update the switch in RootView.swift.
Networking
Section titled “Networking”APIClient wraps URLSession with an interceptor chain (auth, logging). Each service composes typed APIRequest values and asks the client to decode them into a Decodable. Failures surface as AppError. In DEBUG, requests and responses are logged via os.Logger; filter Console.app by your bundle ID and category:network to see traces.
File-by-file
Section titled “File-by-file”App/AppEntry.swift:@mainstruct, builds theRouterandDIContainer, hostsRootView.App/RootView.swift: aNavigationStackbound toRouter.path, switching onRoute.App/DIContainer.swift: the Factory container; auto-edited between markers.Infrastructure/Routing/Route.swift: theRouteenum; auto-edited between markers.Infrastructure/Routing/Router.swift:@ObservablewrappingNavigationPath.Infrastructure/Networking/APIClient.swift: the URLSession client.Infrastructure/Networking/APIRequest.swift: typed request value.Infrastructure/Networking/APIConfig.swift: base URL, headers, auth strategy.Infrastructure/Networking/AppError.swift: the app’s error type.Infrastructure/Auth/TokenStore.swift: actor holding the auth token.Infrastructure/Logging/Log.swift: namespace overos.Logger.
Where templates live
Section titled “Where templates live”Each generator emits a Stencil template from Sources/SwiftspawnKit/Templates/ in the CLI repo. If you want different boilerplate, fork the CLI and edit the templates. There’s no plugin system yet.
- Vocabulary for the words this CLI uses.
- Your first project for the hands-on walkthrough.