Factory
repository·main·Indexed 25 days ago
https://github.com/hmlongco/factoryA modern, container-based dependency injection framework for Swift and SwiftUI. Factory is designed to be lightweight and compile-time safe, supporting architectural patterns like MVVM, VIPER, and Clean Architecture. It provides features such as the @Injected property wrapper, object scopes (singleton, cached, shared), and support for Swift concurrency, including @MainActor and @globalActor isolation. The framework includes FactoryKit for core functionality and FactoryTesting for integration with Swift Testing and XCTest.
What's inside Factory
- Factory is a modern, container-based dependency injection framework for Swift and SwiftUI. It is designed to be adaptable, performant, and compile-time safe. Key features include support for containers, scopes, passed parameters, contexts, decorators, and SwiftUI Previews, all while maintaining a lightweight footprint (under 800 lines of executable code).
Define a Custom Container Trait
mainIf you use a custom container that conforms to
SharedContainer, you can create a custom trait for it.- Attach
@TaskLocalto thesharedinstance of your custom container. - Extend
Traitto define your custom trait using the container's$sharedproperty. - Use the custom trait in your
@Testor@Suitemacros.
// 1. Setup custom container public final class CustomContainer: SharedContainer { @TaskLocal public static var shared = CustomContainer() public let manager = ContainerManager() } // 2. Define the trait extension Trait where Self == ContainerTrait<CustomContainer> { static var customContainer: ContainerTrait<CustomContainer> { .init(shared: CustomContainer.$shared, container: .init()) } } // 3. Use it @Test(.customContainer) func testA() async { // ... }- Attach
Register and resolve @globalActor-isolated types
mainA custom
@globalActorbehaves like the@MainActor. Because a@globalActor-isolated class has an isolated initializer, you must annotate the factory property with that specific global actor to allow the registration closure to run on the correct executor.Use a plain
actorwhen you want an isolated instance you interact with viaawait. Use a@globalActorwhen you want a type isolated to a single shared executor.@globalActor actor BackgroundActor { static let shared = BackgroundActor() } @BackgroundActor final class DataManager { init() { /* isolated to @BackgroundActor */ } func fetch() -> Data { ... } } extension Container { @BackgroundActor var dataManager: Factory<DataManager> { self { DataManager() } } }Configure Scopes and Decorators
mainScopes in Factory 2.0 are applied using modifier syntax on the factory definition. Common modifiers include
.singletonand.shared. You can also use.decoratorto add logic when a factory is resolved.extension Container { var singletonService: Factory<ServiceType> { self { MyService() }.singleton } var decoratedSharedService: Factory<MyServiceType> { self { MyService() } .shared .decorator { print("DECORATING \($0.id)") } } }Mock dependencies for unit testing
mainTo test code in isolation, you can override existing registrations in theContainer.sharedwith mocks, stubs, or spies. You can use the.registermethod or the simplified syntax (omitting.register) to provide a new implementation for a dependency.Use the shared container instance
mainEvery container class defined has a statically allocatedsharedinstance. You can use this for a Service Locator pattern or as an application root container that is passed to other components.Register and resolve plain actors
mainOrdinary actors are the simplest case for Factory. Because an actor's initializer is
nonisolated, it can be built synchronously from any context. You do not need any special annotations on the factory registration or special resolution methods. The isolation only applies when accessing the actor's state, which the compiler handles viaawait.actor OrdinaryActor { private var count = 0 func increment() -> Int { count += 1 return count } } extension Container { var ordinaryActor: Factory<OrdinaryActor> { self { OrdinaryActor() } } } // Resolution let actor = Container.shared.ordinaryActor() let count = await actor.increment()Re-export FactoryKit via a Services module
mainTo avoid importing
FactoryKitin every single module, you can create a dedicatedServicesmodule that acts as an intermediary.In Swift 5.9+, use
public import FactoryKitin an umbrella file within yourServicesmodule. This re-exports all Factory symbols (likeFactory,Container, and@Injected) to any module that importsServices.For Swift 5.8 and earlier, use the
@_exported import FactoryKitattribute.// Services/DependencyContainer.swift public import FactoryKit extension Container { public var accountLoader: Factory<AccountLoading?> { promised() } }Reset Container registrations and scopes
mainYou can reset factories and caches on a specific container. Note that resetting a container only affects that specific instance.
reset(): Resets everything in that container.reset(options: .registration): Restores original factories but leaves caches intact.reset(options: .scope): Resets all scope caches but leaves registrations intact.reset(scope: .cached): Resets a specific scope cache.
// Reset everything based in that container. Container.shared.manager.reset() // Reset all registrations, restoring original factories but leaving caches intact Container.shared.manager.reset(options: .registration) // Reset all scope caches, leaving registrations intact Container.shared.manager.reset(options: .scope) // Reset a specific scope cache while leaving the others intact Container.shared.manager.reset(scope: .cached)Define a Factory in Factory 2.0
mainTo define a factory, extend a
Containerand create a computed variable of typeFactory<ServiceType>. Use theself { ... }syntactic sugar to let the container create the factory for you. The type must be explicitly defined (usually a protocol).extension Container { var service: Factory<ServiceType> { self { MyService() } } }Resolve a Factory
mainYou can obtain an instance of a dependency (resolve it) using several methods:
- Direct Call: Call the factory as a function on a container instance (e.g.,
Container.shared.service()). - @Injected Property Wrapper: Use the
@Injectedproperty wrapper with aKeyPathto the dependency. By default, this looks inContainer.shared. - Custom Container Injection: Specify a custom container in the
@Injectedwrapper.
Important for iOS 17+: When using the
@Observablemacro, you must use@ObservationIgnoredon@Injectedproperties to avoid property wrapper collisions with the@Observablebacking store.// Method 1: Direct call on shared container class ContentViewModel: ObservableObject { private let myService = Container.shared.service() } // Method 2: Using @Injected (defaults to Container.shared) class ContentViewModel: ObservableObject { @Injected(\.myService) private var myService } // Method 2b: Using @Injected with a custom container @Injected(\MyCustomContainer.service) var service: ServiceType // Method 3: Using @Injected with @Observable (iOS 17+) @Observable class ContentViewModel { @ObservationIgnored @Injected(\.myService) private var myService }- Direct Call: Call the factory as a function on a container instance (e.g.,
Register and resolve @MainActor-isolated types
mainWhen a type is isolated to the
@MainActor(like many ViewModels), its initializer is also@MainActor-isolated. To register this in Factory, you must annotate the factory property with@MainActor. This ensures the factory closure runs on the main actor.Most users resolve these from other
@MainActorcontexts (like SwiftUI views or@Injectedproperties in@MainActorclasses), which works seamlessly. However, resolving from a non-isolated context (like a detached task) requires special handling to avoid runtime traps.@MainActor class ContentViewModel { init() { /* isolated to @MainActor */ } func load() async { ... } } extension Container { @MainActor var contentViewModel: Factory<ContentViewModel> { self { ContentViewModel() } } }