@Injected Property Wrapper
The @Injected
property wrapper is WeaveDI's flagship dependency injection solution, inspired by TCA's @Dependency
but optimized for WeaveDI. It provides type-safe, compile-time checked dependency injection with zero configuration overhead.
Overview
@Injected
represents a modern approach to dependency injection in Swift, combining the best practices from The Composable Architecture (TCA) with WeaveDI's powerful container management. It eliminates the need for optional handling while maintaining compile-time safety and provides both KeyPath-based and Type-based access patterns.
Key Benefits:
- ✅ Type-Safe: Compile-time type checking
- ✅ TCA-Style: Familiar API for TCA developers
- ✅ Flexible: Supports both KeyPath and Type-based access
- ✅ Immutable: No
mutating get
required - ✅ Testable: Easy to override in tests
Basic Usage
1. Define InjectedKey
swift
struct APIClientKey: InjectedKey {
static let liveValue: APIClient = APIClientImpl()
static let testValue: APIClient = MockAPIClient()
}
2. Extend InjectedValues
swift
extension InjectedValues {
var apiClient: APIClient {
get { self[APIClientKey.self] }
set { self[APIClientKey.self] = newValue }
}
}
3. Use @Injected
swift
struct MyFeature: Reducer {
@Injected(\.apiClient) var apiClient
func reduce(into state: inout State, action: Action) -> Effect<Action> {
.run { send in
let data = try await apiClient.fetchData()
await send(.dataLoaded(data))
}
}
}
Type-Based Access
You can also use @Injected
with types directly:
swift
extension ExchangeUseCaseImpl: InjectedKey {
public static var liveValue: ExchangeRateInterface {
let repository = UnifiedDI.register(ExchangeRateInterface.self) {
ExchangeRepositoryImpl()
}
return ExchangeUseCaseImpl(repository: repository)
}
}
struct CurrencyFeature: Reducer {
@Injected(ExchangeUseCaseImpl.self) var exchangeUseCase
}
Testing
Override dependencies in tests using withInjectedValues
:
swift
func testFetchData() async {
await withInjectedValues { values in
values.apiClient = MockAPIClient()
} operation: {
let feature = MyFeature()
// Test with mock
}
}
Comparison with @Inject
Feature | @Inject (Deprecated) | @Injected (New) |
---|---|---|
Type Safety | ❌ Optional-based | ✅ Compile-time |
TCA Style | ❌ Different | ✅ Familiar |
KeyPath | ✅ Supported | ✅ Supported |
Type Access | ❌ No | ✅ Supported |
Immutable | ❌ Needs mutating | ✅ Non-mutating |
Testing | ⚠️ Manual | ✅ Built-in |
Migration Guide
From @Inject
swift
// ❌ Old
@Inject var repository: UserRepository?
// ✅ New
@Injected(\.repository) var repository
From Manual Resolution
swift
// ❌ Old
let repository = UnifiedDI.requireResolve(UserRepository.self)
// ✅ New
@Injected(\.repository) var repository
Best Practices
- Prefer KeyPath access - Better discoverability
- Use Type access - Quick prototyping
- Always define testValue - Easier testing
- Keep InjectedKey extensions close - Near type definition
Advanced: Protocol-based Dependencies
swift
protocol ExchangeRateInterface: Sendable {
func getExchangeRates(currency: String) async throws -> ExchangeRates?
}
extension ExchangeUseCaseImpl: InjectedKey {
public static var liveValue: ExchangeRateInterface {
// Return protocol implementation
ExchangeUseCaseImpl(repository: ...)
}
}
extension InjectedValues {
var exchangeUseCase: ExchangeRateInterface {
get { self[ExchangeUseCaseImpl.self] }
set { self[ExchangeUseCaseImpl.self] = newValue }
}
}
Real-World Example: Currency Exchange App
swift
// 1. Define InjectedKey
extension ExchangeUseCaseImpl: InjectedKey {
public static var liveValue: ExchangeRateInterface {
let repository = UnifiedDI.register(ExchangeRateInterface.self) {
ExchangeRepositoryImpl()
}
return ExchangeUseCaseImpl(repository: repository)
}
}
// 2. Extend InjectedValues
public extension InjectedValues {
var exchangeUseCase: ExchangeRateInterface {
get { self[ExchangeUseCaseImpl.self] }
set { self[ExchangeUseCaseImpl.self] = newValue }
}
}
// 3. Use in Reducer
struct CurrencyFeature: Reducer {
@Injected(\.exchangeUseCase) var exchangeUseCase
func reduce(into state: inout State, action: Action) -> Effect<Action> {
case .fetchRates(let currency):
return .run { send in
let rates = try await exchangeUseCase.getExchangeRates(currency: currency)
await send(.ratesLoaded(rates))
}
}
}
See Also
- @Factory - For creating new instances each time
- Property Wrappers Guide - Complete property wrapper documentation
- Testing Guide - Advanced testing patterns