Skip to content

Factory

Developer UtilitiesDependency InjectionSwift

What it is

Factory is a compile-time-safe dependency injection container for Swift, using static properties and property wrappers rather than runtime type lookup.

Declare each dependency as a computed Factory on a Container extension, then inject with @Injected. Scopes control lifetime.

Installation

.package(url: "https://github.com/hmlongco/Factory.git", from: "2.4.0")

Getting started

The smallest useful thing you can do with it, and what each part means.

Typed registrations
import Factory

extension Container {
    var apiClient: Factory<APIClient> {
        self { URLSessionAPIClient() }.singleton
    }

    var bookRepository: Factory<BookRepository> {
        self { RemoteBookRepository(api: self.apiClient()) }
    }
}

final class BookViewModel {
    // Resolved lazily; the type is checked at compile time.
    @Injected(\.bookRepository) private var repository

    func load() async throws -> [Book] {
        try await repository.fetchAll()
    }
}
There is no resolve returning an optional and no force unwrap. Renaming or removing a registration breaks the build, which is the whole point compared with runtime containers.

Advanced usage

Where the library earns its place over a simpler alternative.

Test overrides and scopes
// Override for a single test, then reset.
final class BookViewModelTests: XCTestCase {
    override func setUp() {
        super.setUp()
        Container.shared.bookRepository.register { MockBookRepository() }
    }

    override func tearDown() {
        Container.shared.reset()   // essential, or overrides leak between tests
        super.tearDown()
    }
}

// Scopes
self { Service() }.singleton    // one for the app lifetime
self { Service() }.cached       // until reset
self { Service() }.shared       // while something holds a strong reference
self { Service() }              // new every time (default)
Forgetting Container.shared.reset() in tearDown is the most common Factory mistake — an override from one test then silently applies to every test after it.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Tests interfere with each other
A registration override persisted. Call Container.shared.reset() in tearDown.
A circular dependency causes a stack overflow
Two factories resolve each other. Break the cycle with a lazy property or a protocol boundary.

Best practices

  • Always reset the container in tearDown so overrides do not leak between tests.
  • Prefer @Injected over resolving manually so dependencies are visible on the type.
  • Choose the narrowest scope that works; .singleton retains whatever it holds forever.
  • Consider Factory over Swinject when compile-time safety matters more than assembly organisation.

Background

Why it exists, and what it was reacting to.

Factory was written as a response to Swinject's runtime resolution: because dependencies are declared as typed static properties, a missing registration is a compile error rather than a crash.