Skip to content

Dependency injection

How Factory is wired into a generated project, how services are registered, and how to swap them in tests and previews.

Generated projects use Factory for dependency injection. Factory is a property-wrapper-driven container that’s small enough to read in a sitting and powerful enough to handle real apps.

  • Container: Factory’s container type. Generated projects extend the shared Container to add app-specific factories.
  • @Injected(\.foo): a property wrapper used inside ViewModels (and other consumers) to receive a dependency.
  • Factory<T>: a single registration. It returns a T each time it’s called.
  • DIContainer.swift: where every project-local factory is declared.

generate service Movie adds this between the auto-managed markers in DIContainer.swift:

App/DIContainer.swift
extension Container {
// MARK: - Service Factories (auto-generated)
var movieService: Factory<MovieService> {
self { MovieServiceImpl() }
}
// MARK: - End auto-generated
}

self { ... } creates a Factory whose default scope is unique: every resolution returns a new instance. For singletons, edit the line:

var analytics: Factory<Analytics> {
self { Analytics(env: .production) }.singleton
}

Custom-scoped or constructed-arg factories belong outside the markers, so the CLI doesn’t overwrite them.

Features/Movies/MovieList/MovieListViewModel.swift
@MainActor @Observable
final class MovieListViewModel {
@Injected(\.movieService) private var service
var state: LoadState<[Movie]> = .idle
func load() async {
state = .loading
do {
let movies = try await service.fetch()
state = .loaded(movies)
} catch {
state = .failed(error as? AppError ?? .unexpected)
}
}
}

@Injected(\.movieService) resolves through the shared Container lazily, the first time the property is read.

Factory’s container is a singleton you can mutate. Tests register a mock for the duration of the test:

Tests/MovieAppTests/Features/Movies/MovieList/MovieListViewModelTests.swift
final class MovieListViewModelTests: XCTestCase {
override func tearDown() {
Container.shared.reset()
}
func test_load_success() async {
let mock = MockMovieService()
mock.fetchResult = .success([Movie.fixture])
Container.shared.movieService.register { mock }
let vm = MovieListViewModel()
await vm.load()
XCTAssertEqual(vm.state, .loaded([Movie.fixture]))
}
}

Container.shared.reset() in tearDown ensures a clean slate between tests.

Same trick, in a preview helper:

#Preview {
let _ = {
Container.shared.movieService.register { MockMovieService() }
}()
return MovieListView()
}

The mock’s canned responses make previews work offline and without configuration.

generate screen MovieList on Movies --uses MovieService,UserService does two things:

  1. Registers each service in DIContainer.swift between the markers (idempotent: existing entries are not duplicated).

  2. Generates the ViewModel with @Injected properties for each:

    @Injected(\.movieService) private var movieService
    @Injected(\.userService) private var userService

If DIContainer.swift is missing markers (legacy projects), the CLI prints the lines you would have needed to add and writes the ViewModel anyway.

SwiftUI’s @Environment works for view-tree-scoped values, but ViewModels are not in the view tree. Factory gives a single resolution mechanism that works the same in views, ViewModels, services, and tests, and stays decoupled from SwiftUI.

If you’d prefer a different DI tool (Resolver, Swinject, hand-rolled), the templates are the place to swap. The CLI doesn’t have a flag for “use a different DI library” today.