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.
The pieces
Section titled “The pieces”Container: Factory’s container type. Generated projects extend the sharedContainerto 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 aTeach time it’s called.- DIContainer.swift: where every project-local factory is declared.
Registering a service
Section titled “Registering a service”generate service Movie adds this between the auto-managed markers in 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.
Consuming a service in a ViewModel
Section titled “Consuming a service in a ViewModel”@MainActor @Observablefinal 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.
Swapping in tests
Section titled “Swapping in tests”Factory’s container is a singleton you can mutate. Tests register a mock for the duration of the test:
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.
Swapping in previews
Section titled “Swapping in previews”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.
How --uses wires DI
Section titled “How --uses wires DI”generate screen MovieList on Movies --uses MovieService,UserService does two things:
-
Registers each service in
DIContainer.swiftbetween the markers (idempotent: existing entries are not duplicated). -
Generates the ViewModel with
@Injectedproperties 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.
Why Factory and not @Environment
Section titled “Why Factory and not @Environment”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.
See also
Section titled “See also”- Architecture
- Networking flow
- Markers
generate servicegenerate screenwith--uses.