Skip to content

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.

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.

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.

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.

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.

A small set of types every project shares:

  • APIClient: a URLSession-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 @Observable that 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 over os.Logger.

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.

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 movieService

In tests you swap the implementation:

Container.shared.movieService.register { MockMovieService() }

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.

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.

  • App/AppEntry.swift: @main struct, builds the Router and DIContainer, hosts RootView.
  • App/RootView.swift: a NavigationStack bound to Router.path, switching on Route.
  • App/DIContainer.swift: the Factory container; auto-edited between markers.
  • Infrastructure/Routing/Route.swift: the Route enum; auto-edited between markers.
  • Infrastructure/Routing/Router.swift: @Observable wrapping NavigationPath.
  • 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 over os.Logger.

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.