Software Design

Swift DI Library Factory: Summary and Differences from Swinject

Factory is a Swift DI library that catches missing dependency registrations at compile time. As of July 2026, this guide covers version 3.3.1, basic usage, scope declarations, test and preview injection, and differences from Swinject.

5 min read
Cover image for Swift DI Library Factory: Summary and Differences from Swinject

In the previous article, we covered the final letter of SOLID, DIP (the Dependency Inversion Principle). Once you understand the principle, the next question naturally follows: “So, who assembles these dependencies in a real app, and how?”

For small projects, manual DI (Dependency Injection) through initializers is enough. But as the number of screens grows into the dozens and the dependency graph deepens, assembly code itself becomes a burden. Today, we’ll look at Factory, a Swift DI library that fits this point well. This guide is based on the latest version as of July 2026: 3.3.1.


How Far Can Manual DI Go?

Code that follows DIP looked roughly like this.

protocol NetworkProviding {
    func fetch(_ url: URL) async throws -> Data
}

final class OrderViewModel {
    private let network: NetworkProviding

    init(network: NetworkProviding) {
        self.network = network
    }
}

The code depends on protocols, while implementations are injected from outside. No library is needed so far. The problem is the assembly layer.

// an assembly point somewhere(Composition Root)
let network = NetworkProvider()
let repository = OrderRepository(network: network)
let analytics = AnalyticsService(network: network)
let viewModel = OrderViewModel(repository: repository, analytics: analytics)

When dependencies reach three or four layers deep, initialization code like this gets repeated across screens. Add one dependency to an intermediate layer, and every initialization path through it must be modified. That’s when the temptation to escape into a singleton grows—but you already know how singletons get in the way of testing. A DI container takes over this assembly problem.

What Makes Factory Different: Compile-Time Safety

For a long time, Swinject was treated as the standard Swift DI library. It registers dependencies by string or type and retrieves them with resolve(), so a missed registration still compiles and fails with nil at runtime. You have to run the app to discover the mistake.

Factory turns this model around. Because dependencies are defined as computed properties on Container, referencing a nonexistent dependency simply fails to compile. Typos and missing registrations are caught during the build.

It is also a lightweight library with fewer than 1,000 lines of executable code, and it runs using pure Swift without a compile-time code-generation script. Unlike Needle, it requires no extra tool in the build pipeline.

Factory Basics in 3 Minutes

Install it with SPM (Swift Package Manager). The package URL is https://github.com/hmlongco/Factory, and from 3.x onward, import it with FactoryKit.

Register dependencies by adding computed properties in a Container extension.

import FactoryKit

extension Container {
    var networkService: Factory<NetworkProviding> {
        self { NetworkProvider() }
    }
    var orderRepository: Factory<OrderRepositoryType> {
        self { OrderRepository(network: self.networkService()) }
    }
}

There are three ways to retrieve them.

// 1. Property-wrapper injection
final class OrderViewModel {
    @Injected(\.orderRepository) private var repository
}

// 2. Direct call
let repository = Container.shared.orderRepository()

// 3. Initializer injection — leave only assembly to the container
extension Container {
    var orderViewModel: Factory<OrderViewModel> {
        self { OrderViewModel(repository: self.orderRepository()) }
    }
}

The third approach is worth noting. The class itself remains pure initializer injection and knows nothing about the library, while the container handles only assembly. It aligns exactly with the principle from the DIP article: “Assembling implementations is the outside’s responsibility.”

In SwiftUI, you can receive an Observable view model directly with @InjectedObservable.

struct OrderView: View {
    @InjectedObservable(\.orderViewModel) var viewModel
}
An illustration of a container assembling dependency modules onto app screens via a conveyor belt
Declare them in the registry, and the container handles assembly

Scopes: Declare Instance Lifetimes

Another reason to use a DI container is instance-lifetime management. With Factory, you only need to add one modifier to the registry.

extension Container {
    var networkService: Factory<NetworkProviding> {
        self { NetworkProvider() }.singleton
    }
    var imageCache: Factory<ImageCaching> {
        self { ImageCache() }.cached
    }
}
  • unique — The default. Creates a new instance on every request.
  • singleton — Shares one instance across the entire app.
  • cached — Returns the same instance until the cache is reset.
  • shared — Keeps the instance only while someone holds a strong reference; it is released when nobody uses it.

The difference from creating a global singleton directly is that the lifetime is declared in one registry instead of being scattered throughout the code as static let. To change singleton to cached later, you only change one modifier.

Where Testing and Previews Show Their Value

The most practical reason to introduce DI is testing. Factory lets you override a registration in place.

import FactoryTesting

@Suite(.container)  // Isolate the container for each test
struct OrderViewModelTests {
    @Test func loadsOrders() async {
        Container.shared.orderRepository { MockOrderRepository() }
        let viewModel = Container.shared.orderViewModel()
        await viewModel.load()
        #expect(viewModel.orders.count == 3)
    }
}

In Swift Testing, adding the .container trait to the FactoryTesting target keeps container state from leaking between tests. You can inject mocks the same way in SwiftUI previews.

#Preview {
    Container.shared.orderRepository { MockOrderRepository() }
    return OrderView()
}

There is also a context modifier that automatically swaps implementations only in a specific execution environment. For example, if you want stub analytics only in debug builds:

container.analytics.onDebug { StubAnalyticsEngine() }

Using the same technique, you can declare overrides for tests, previews, and simulator environments in the registry.

An illustration of replacing a machine slot’s production module with a mock module for testing
Tests plug in mocks instead of the real implementation

What Changed in Factory 3.x

Here are the key changes for those upgrading from 2.x.

  • Import name changeimport Factory changed to import FactoryKit. Most of the migration is this replacement.
  • Full Swift 6 Strict Concurrency support — When registering an @MainActor view model, 2.x required repeating @MainActor in inside the closure, but 3.x handles it with only the annotation on the factory declaration.
  • SPM-only — CocoaPods support has been discontinued. CocoaPods projects must stay on Factory 2.5.3 or embed the source directly.
  • Swift Testing support — Official test-isolation support, including the .container trait shown above, has been added.
A Factory architecture diagram showing the Container registry shared by app code, tests, and previews
App code, tests, and previews share one registry

Summary

DIP tells us to depend on abstractions rather than concrete types; Factory reduces the cost of maintaining that direction in a real codebase. Compile-time safety turns missing registrations into build errors, while scopes and mock replacement take only a few declarations.

There is no need to rewrite a project already working well with Swinject, but for a new project, Factory is worth considering as the default choice. It is lightweight and easy to adopt, and if you later dislike it, you can remove only the container while keeping the initializer-injection structure.

If manual DI has started to hold you back with assembly code, give it a try.


References

Continue reading