@Injected - Modern Dependency Injection
Overview
@Injected
is WeaveDI's flagship property wrapper, inspired by TCA's @Dependency
but optimized for WeaveDI. It provides type-safe, compile-time checked dependency injection with zero configuration overhead.
Why @Injected?
- ✅ 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 (Legacy) | @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 for better discoverability
- Use Type access for quick prototyping
- Always define testValue for easier testing
- Keep InjectedKey extensions close to the 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
- Property Wrappers - Complete property wrapper guide
- Testing Guide - Advanced testing patterns
- TCA Integration - Using @Injected with TCA