Skip to content

@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

  1. Prefer KeyPath access - Better discoverability
  2. Use Type access - Quick prototyping
  3. Always define testValue - Easier testing
  4. 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

Released under the MIT License.