Contents

brunogama/SwinjectMacros

A Collection of macros to interact with Swinject

Why SwinjectMacros?

Traditional dependency injection in Swift requires significant boilerplate code:

Before: Traditional Swinject (Extensive Boilerplate)

// Service Definition
class UserService {
    private let apiClient: APIClient
    private let database: DatabaseService
    private let logger: LoggerService

    init(apiClient: APIClient, database: DatabaseService, logger: LoggerService) {
        self.apiClient = apiClient
        self.database = database
        self.logger = logger
    }
}

// Manual Registration (Repetitive & Error-Prone)
class AppAssembly: Assembly {
    func assemble(container: Container) {
        container.register(APIClient.self) { _ in
            APIClientImpl()
        }.inObjectScope(.container)

        container.register(DatabaseService.self) { _ in
            DatabaseServiceImpl()
        }.inObjectScope(.container)

        container.register(LoggerService.self) { _ in
            LoggerServiceImpl()
        }.inObjectScope(.graph)

        container.register(UserService.self) { resolver in
            UserService(
                apiClient: resolver.resolve(APIClient.self)!,
                database: resolver.resolve(DatabaseService.self)!,
                logger: resolver.resolve(LoggerService.self)!
            )
        }.inObjectScope(.graph)
    }
}

After: SwinjectMacros (Clean and Concise)

// Service Definition with Auto-Registration
@Injectable
class UserService {
    private let apiClient: APIClient
    private let database: DatabaseService
    private let logger: LoggerService

    init(apiClient: APIClient, database: DatabaseService, logger: LoggerService) {
        self.apiClient = apiClient
        self.database = database
        self.logger = logger
    }
}

@Injectable(scope: .container)
class APIClientImpl: APIClient { /* implementation */ }

@Injectable(scope: .container)
class DatabaseServiceImpl: DatabaseService { /* implementation */ }

@Injectable
class LoggerServiceImpl: LoggerService { /* implementation */ }

// That's it! Registration is automatically generated at compile-time

Key Benefits

  • Zero Runtime Overhead: All code generation happens at compile-time
  • Type Safety: Full Swift type system integration with compile-time validation
  • Dramatically Less Code: Reduce dependency injection boilerplate by 80%+
  • Better Error Messages: Clear, actionable compile-time diagnostics
  • Performance: No reflection, no runtime lookups - pure Swift performance
  • Testing Made Easy: Automatic mock generation and test container setup
  • Factory Patterns: Automatic factory generation for services with runtime parameters

Table of Contents

Getting Started

Core Macros Guide

- Why @Injectable? - How It Works - Basic Usage - Advanced Configuration - Object Scopes - Named Services - Optional Dependencies - Smart Dependency Classification

- Why @AutoFactory? - How It Works - Basic Usage - Advanced Configuration - Async/Throws Support - Custom Factory Names - Factory Registration and Usage

- Why @TestContainer? - How It Works - Basic Usage - Advanced Configuration - Custom Mock Prefix - Custom Scope - Manual Mock Control - Spy Generation (Future Feature)

- Why @Interceptor? - How It Works - Basic Usage - Advanced Configuration - Async/Throws Support - Static Method Interception - Creating Custom Interceptors - Built-in Interceptors - LoggingInterceptor - PerformanceInterceptor - Interceptor Registration - Real-World Example: E-commerce Service - Performance Benefits

Guides and Best Practices

- Service Design Guidelines - Scope Selection - Factory vs Injectable Decision - Testing Strategy

- Circular Dependencies - Runtime Parameters in @Injectable - Missing Protocol Registrations

๐Ÿ”— Resources


๐Ÿ“ฆ Installation

Swift Package Manager

Add SwinjectMacros to your project via Xcode or by adding it to your Package.swift:

dependencies: [
    .package(url: "https://github.com/brunogama/SwinjectMacros.git", from: "1.0.2")
],
targets: [
    .target(
        name: "YourTarget",
        dependencies: ["SwinjectMacros"]
    )
]

๐Ÿ“‹ Requirements

  • Swift 5.9+ (Required for macro support)
  • iOS 15.0+ / macOS 12.0+ / watchOS 8.0+ / tvOS 15.0+
  • Xcode 15.0+

๐ŸŽ“ Core Macros Guide

### 1. @Injectable - Automatic Service Registration

The `@Injectable` macro automatically generates dependency injection registration code for your services.

#### ๐Ÿค” **Why @Injectable?**

**Problem**: Manually writing registration code for every service is:

- Repetitive and error-prone
- Hard to maintain when dependencies change
- Requires updating multiple places when refactoring
- Easy to forget registrations for new services

**Solution**: `@Injectable` analyzes your service's initializer and automatically generates the correct registration code with proper dependency resolution.

#### ๐Ÿ“– **How It Works**

The macro examines your class/struct initializer and:

1. **Identifies service dependencies** (types ending in Service, Repository, Client, etc.)
1. **Generates resolver calls** for each dependency
1. **Creates a static `register(in:)` method**
1. **Adds `Injectable` protocol conformance**

#### ๐Ÿ”ง **Basic Usage**

```swift
import SwinjectMacros
import Swinject

// Simple service with dependencies
@Injectable
class UserService {
    private let apiClient: APIClient
    private let database: DatabaseService

    init(apiClient: APIClient, database: DatabaseService) {
        self.apiClient = apiClient
        self.database = database
    }

    func getUser(id: String) async throws -> User {
        let userData = try await apiClient.fetchUser(id: id)
        try await database.save(userData)
        return User(from: userData)
    }
}
```

**Generated Code** (you don't write this!):

```swift
extension UserService: Injectable {
    static func register(in container: Container) {
        container.register(UserService.self) { resolver in
            UserService(
                apiClient: resolver.resolve(APIClient.self)!,
                database: resolver.resolve(DatabaseService.self)!
            )
        }.inObjectScope(.graph)
    }
}
```

#### โš™๏ธ **Advanced Configuration**

##### Object Scopes

Control the lifecycle of your services:

```swift
@Injectable(scope: .container)  // Singleton - one instance per container
class DatabaseService {
    init() { /* expensive setup */ }
}

@Injectable(scope: .graph)      // Default - new instance per object graph
class UserService {
    init(database: DatabaseService) { /* ... */ }
}

@Injectable(scope: .singleton)  // Global singleton - one instance ever
class ConfigurationService {
    init() { /* app-wide config */ }
}
```

##### Named Services

Register multiple implementations of the same protocol:

```swift
protocol PaymentProcessor {
    func process(payment: Payment) async throws
}

@Injectable(name: "stripe")
class StripePaymentProcessor: PaymentProcessor {
    init(apiKey: String) { /* ... */ }
}

@Injectable(name: "paypal")
class PayPalPaymentProcessor: PaymentProcessor {
    init(clientId: String, secret: String) { /* ... */ }
}

// Usage
let stripeProcessor = container.resolve(PaymentProcessor.self, name: "stripe")
let paypalProcessor = container.resolve(PaymentProcessor.self, name: "paypal")
```

##### Optional Dependencies

Handle optional dependencies gracefully:

```swift
@Injectable
class AnalyticsService {
    private let logger: LoggerService?  // Optional dependency
    private let database: DatabaseService // Required dependency

    init(logger: LoggerService?, database: DatabaseService) {
        self.logger = logger
        self.database = database
    }
}

// Generated registration handles optionals correctly:
// logger: resolver.resolve(LoggerService.self)  // No force unwrap for optionals
// database: resolver.resolve(DatabaseService.self)!  // Force unwrap for required
```

#### ๐ŸŽฏ **Smart Dependency Classification**

The macro automatically classifies your parameters:

| Parameter Type             | Classification          | Resolution Strategy                  |
| -------------------------- | ----------------------- | ------------------------------------ |
| `UserService`, `APIClient` | Service Dependency      | `resolver.resolve(Type.self)!`       |
| `any DatabaseProtocol`     | Protocol Dependency     | `resolver.resolve(Protocol.self)!`   |
| `String`, `Int`, `Bool`    | Runtime Parameter       | โš ๏ธ Warning - consider `@AutoFactory` |
| `String = "default"`       | Configuration Parameter | Use default value                    |

### 2. @AutoFactory - Factory Pattern Generation

The `@AutoFactory` macro generates factory protocols and implementations for services that need runtime parameters.

#### ๐Ÿค” **Why @AutoFactory?**

**Problem**: Some services need both injected dependencies AND runtime parameters:

- User input (search terms, user IDs, etc.)
- Dynamic configuration
- Request-specific data
- You can't pre-register these in the container

**Traditional Solution** (lots of boilerplate):

```swift
// Manual factory - lots of repetitive code
protocol UserSearchServiceFactory {
    func makeUserSearchService(query: String, filters: [Filter]) -> UserSearchService
}

class UserSearchServiceFactoryImpl: UserSearchServiceFactory {
    private let resolver: Resolver

    init(resolver: Resolver) {
        self.resolver = resolver
    }

    func makeUserSearchService(query: String, filters: [Filter]) -> UserSearchService {
        return UserSearchService(
            apiClient: resolver.resolve(APIClient.self)!,
            database: resolver.resolve(DatabaseService.self)!,
            query: query,
            filters: filters
        )
    }
}
```

**SwinjectMacros Solution** (automatic):

```swift
@AutoFactory
class UserSearchService {
    private let apiClient: APIClient      // Injected dependency
    private let database: DatabaseService // Injected dependency
    private let query: String            // Runtime parameter
    private let filters: [Filter]        // Runtime parameter

    init(apiClient: APIClient, database: DatabaseService, query: String, filters: [Filter]) {
        // implementation
    }
}
```

#### ๐Ÿ“– **How It Works**

The macro analyzes your initializer and:

1. **Separates injected dependencies** from runtime parameters
1. **Generates a factory protocol** with a `make` method for runtime parameters only
1. **Generates a factory implementation** that resolves dependencies and accepts runtime parameters
1. **Handles async/throws automatically**

#### ๐Ÿ”ง **Basic Usage**

```swift
@AutoFactory
class ReportGenerator {
    private let database: DatabaseService  // Injected
    private let emailService: EmailService // Injected
    private let reportType: ReportType     // Runtime parameter
    private let dateRange: DateRange       // Runtime parameter

    init(database: DatabaseService, emailService: EmailService,
         reportType: ReportType, dateRange: DateRange) {
        self.database = database
        self.emailService = emailService
        self.reportType = reportType
        self.dateRange = dateRange
    }

    func generateAndSend() async throws {
        let report = try await database.generateReport(type: reportType, range: dateRange)
        try await emailService.send(report)
    }
}
```

**Generated Code**:

```swift
// Factory Protocol
protocol ReportGeneratorFactory {
    func makeReportGenerator(reportType: ReportType, dateRange: DateRange) -> ReportGenerator
}

// Factory Implementation
class ReportGeneratorFactoryImpl: ReportGeneratorFactory, BaseFactory {
    let resolver: Resolver

    init(resolver: Resolver) {
        self.resolver = resolver
    }

    func makeReportGenerator(reportType: ReportType, dateRange: DateRange) -> ReportGenerator {
        ReportGenerator(
            database: resolver.resolve(DatabaseService.self)!,
            emailService: resolver.resolve(EmailService.self)!,
            reportType: reportType,
            dateRange: dateRange
        )
    }
}
```

#### โš™๏ธ **Advanced Configuration**

##### Async/Throws Support

```swift
@AutoFactory(async: true, throws: true)
class AsyncDataProcessor {
    private let apiClient: APIClient  // Injected
    private let data: Data           // Runtime parameter

    init(apiClient: APIClient, data: Data) async throws {
        self.apiClient = apiClient
        // Async initialization logic
        try await apiClient.validateData(data)
    }
}

// Generated factory method signature:
// func makeAsyncDataProcessor(data: Data) async throws -> AsyncDataProcessor
```

##### Custom Factory Names

```swift
@AutoFactory(name: "CustomReportFactory")
class ReportService {
    init(database: DatabaseService, reportId: String) { /* ... */ }
}

// Generates: protocol CustomReportFactory { ... }
// Instead of: protocol ReportServiceFactory { ... }
```

##### Factory Registration and Usage

```swift
// In your assembly
class AppAssembly: Assembly {
    func assemble(container: Container) {
        // Register your services
        container.register(DatabaseService.self) { _ in DatabaseServiceImpl() }
        container.register(EmailService.self) { _ in EmailServiceImpl() }

        // Register the factory
        container.registerFactory(ReportGeneratorFactory.self)
    }
}

// Usage in your application
class ReportsViewController: UIViewController {
    private let reportFactory: ReportGeneratorFactory

    init(reportFactory: ReportGeneratorFactory) {
        self.reportFactory = reportFactory
        super.init(nibName: nil, bundle: nil)
    }

    @IBAction func generateReport() {
        let generator = reportFactory.makeReportGenerator(
            reportType: .monthly,
            dateRange: DateRange(start: startDate, end: endDate)
        )

        Task {
            try await generator.generateAndSend()
        }
    }
}
```

### 3. @TestContainer - Test Mock Generation

The `@TestContainer` macro automatically generates test container setup with mocks for your test classes.

#### ๐Ÿค” **Why @TestContainer?**

**Problem**: Setting up dependency injection for tests is tedious:

- Creating mock objects for every dependency
- Registering all mocks in the test container
- Maintaining test setup as dependencies change
- Ensuring test isolation

**Traditional Approach** (lots of test boilerplate):

```swift
class UserServiceTests: XCTestCase {
    var container: Container!
    var mockAPIClient: MockAPIClient!
    var mockDatabase: MockDatabaseService!
    var mockLogger: MockLoggerService!
    var userService: UserService!

    override func setUp() {
        super.setUp()
        container = Container()

        // Create all mocks manually
        mockAPIClient = MockAPIClient()
        mockDatabase = MockDatabaseService()
        mockLogger = MockLoggerService()

        // Register all mocks manually
        container.register(APIClient.self) { _ in self.mockAPIClient }
        container.register(DatabaseService.self) { _ in self.mockDatabase }
        container.register(LoggerService.self) { _ in self.mockLogger }

        userService = container.resolve(UserService.self)!
    }
}
```

**SwinjectMacros Approach** (automatic):

```swift
@TestContainer
class UserServiceTests: XCTestCase {
    var apiClient: APIClient!
    var database: DatabaseService!
    var logger: LoggerService!

    // Container setup is automatically generated!
}
```

#### ๐Ÿ“– **How It Works**

The macro scans your test class properties and:

1. **Identifies service properties** (types ending in Service, Repository, Client, etc.)
1. **Generates a `setupTestContainer()` method** that creates and configures a container
1. **Generates mock registration helpers** for each service type
1. **Supports custom mock prefixes and scopes**

#### ๐Ÿ”ง **Basic Usage**

```swift
import XCTest
import SwinjectMacros

@TestContainer
class UserServiceTests: XCTestCase {
    var container: Container!

    // These properties are detected as services needing mocks
    var apiClient: APIClient!
    var database: DatabaseService!
    var logger: LoggerService!

    override func setUp() {
        super.setUp()
        container = setupTestContainer() // Generated method!

        // Services are automatically registered with mocks
        apiClient = container.resolve(APIClient.self)!
        database = container.resolve(DatabaseService.self)!
        logger = container.resolve(LoggerService.self)!
    }

    func testUserCreation() {
        // Your test logic here
        // All dependencies are automatically mocked
    }
}
```

**Generated Code**:

```swift
extension UserServiceTests {
    func setupTestContainer() -> Container {
        let container = Container()

        registerAPIClient(mock: MockAPIClient())
        registerDatabaseService(mock: MockDatabaseService())
        registerLoggerService(mock: MockLoggerService())

        return container
    }

    func registerAPIClient(mock: APIClient) {
        container.register(APIClient.self) { _ in mock }.inObjectScope(.graph)
    }

    func registerDatabaseService(mock: DatabaseService) {
        container.register(DatabaseService.self) { _ in mock }.inObjectScope(.graph)
    }

    func registerLoggerService(mock: LoggerService) {
        container.register(LoggerService.self) { _ in mock }.inObjectScope(.graph)
    }
}
```

#### โš™๏ธ **Advanced Configuration**

##### Custom Mock Prefix

```swift
@TestContainer(mockPrefix: "Stub")
class UserServiceTests: XCTestCase {
    var apiClient: APIClient!
    var database: DatabaseService!
}

// Generates: StubAPIClient(), StubDatabaseService()
// Instead of: MockAPIClient(), MockDatabaseService()
```

##### Custom Scope

```swift
@TestContainer(scope: .container)
class UserServiceTests: XCTestCase {
    var database: DatabaseService! // Will be registered as singleton
}
```

##### Manual Mock Control

```swift
@TestContainer(autoMock: false)
class UserServiceTests: XCTestCase {
    var apiClient: APIClient!

    override func setUp() {
        super.setUp()
        container = setupTestContainer()

        // Provide your own mock implementation
        let customMock = MyCustomAPIClientMock()
        registerAPIClient(mock: customMock)

        apiClient = container.resolve(APIClient.self)!
    }
}
```

##### Spy Generation (Future Feature)

```swift
@TestContainer(generateSpies: true)
class UserServiceTests: XCTestCase {
    var apiClient: APIClient!

    func testAPIClientCalled() {
        // Generated spy functionality
        userService.performAction()

        XCTAssertEqual(apiClientSpy.fetchUserCalls.count, 1)
        XCTAssertEqual(apiClientSpy.fetchUserCalls.first?.userId, "123")
    }
}
```

### 4. @Interceptor - Aspect-Oriented Programming

The `@Interceptor` macro brings powerful aspect-oriented programming (AOP) capabilities to Swift, allowing you to implement cross-cutting concerns like logging, security, caching, and validation without cluttering your business logic.

#### ๐Ÿค” **Why @Interceptor?**

**Problem**: Cross-cutting concerns create code duplication and coupling:

- Logging scattered throughout business methods
- Security checks mixed with business logic
- Performance monitoring code everywhere
- Error handling repeated in every method
- Caching logic coupled to business operations

**Traditional Approach** (scattered concerns):

```swift
class PaymentService {
    func processPayment(amount: Double, cardToken: String) -> PaymentResult {
        // Logging
        logger.log("Processing payment: \(amount)")
        let startTime = Date()

        // Security validation
        guard SecurityValidator.validateToken(cardToken) else {
            logger.error("Invalid card token")
            throw PaymentError.invalidToken
        }

        // Business logic (buried in boilerplate)
        let result = doActualPaymentProcessing(amount: amount, token: cardToken)

        // More logging
        let duration = Date().timeIntervalSince(startTime)
        logger.log("Payment completed in \(duration)ms")

        // Audit logging
        auditLogger.log("Payment processed: \(result)")

        return result
    }
}
```

#### โœ… **With @Interceptor** (clean separation):

```swift
class PaymentService {
    @Interceptor(
        before: ["SecurityInterceptor", "LoggingInterceptor"],
        after: ["AuditInterceptor", "PerformanceInterceptor"]
    )
    func processPayment(amount: Double, cardToken: String) -> PaymentResult {
        // Pure business logic - no clutter!
        return doActualPaymentProcessing(amount: amount, token: cardToken)
    }
}
```

#### ๐Ÿ“– **How It Works**

The `@Interceptor` macro generates an intercepted version of your method that:

1. **Creates rich context** with method name, parameters, types, and execution metadata
1. **Executes before interceptors** in specified order for setup/validation
1. **Calls your original method** with full error handling
1. **Executes after interceptors** in reverse order (LIFO) for cleanup
1. **Handles errors** with dedicated error interceptors
1. **Provides performance metrics** with execution timing

#### ๐Ÿ”ง **Basic Usage**

```swift
// Simple logging interceptor
@Interceptor(before: ["LoggingInterceptor"])
func createUser(userData: UserData) -> User {
    return UserRepository.create(userData)
}

// Multiple interceptor types
@Interceptor(
    before: ["ValidationInterceptor", "SecurityInterceptor"],
    after: ["CacheInterceptor", "NotificationInterceptor"],
    onError: ["ErrorReportingInterceptor"]
)
func updateUserProfile(userId: String, profile: UserProfile) throws -> UserProfile {
    return try UserRepository.update(userId: userId, profile: profile)
}
```

#### โš™๏ธ **Advanced Configuration**

##### Async/Throws Support

```swift
// Async method interception
@Interceptor(before: ["AsyncSecurityInterceptor"])
func fetchUserData(userId: String) async throws -> UserData {
    return try await APIClient.fetchUser(userId)
}

// Error handling with interceptors
@Interceptor(onError: ["ErrorTransformInterceptor", "AlertingInterceptor"])
func riskyOperation() throws -> Result {
    return try performRiskyWork()
}
```

##### Static Method Interception

```swift
class UtilityService {
    @Interceptor(before: ["LoggingInterceptor"])
    static func validateInput(data: String) -> Bool {
        return InputValidator.validate(data)
    }
}
```

#### ๐Ÿ› ๏ธ **Creating Custom Interceptors**

All interceptors must conform to the `MethodInterceptor` protocol:

```swift
class CustomLoggingInterceptor: MethodInterceptor {
    func before(context: InterceptorContext) throws {
        print("๐Ÿš€ [\(context.executionId.uuidString.prefix(8))] Starting \(context.methodName)")
        print("   Parameters: \(context.parameters)")
    }

    func after(context: InterceptorContext, result: Any?) throws {
        print("โœ… [\(context.executionId.uuidString.prefix(8))] Completed in \(context.executionTime)ms")
        if let result = result {
            print("   Result: \(result)")
        }
    }

    func onError(context: InterceptorContext, error: Error) throws {
        print("โŒ [\(context.executionId.uuidString.prefix(8))] Failed: \(error)")
        // Transform or re-throw error as needed
        throw error
    }
}
```

#### ๐Ÿญ **Built-in Interceptors**

SwinjectMacros provides several production-ready interceptors:

##### LoggingInterceptor

```swift
// Provides structured logging with execution IDs
InterceptorRegistry.register(interceptor: LoggingInterceptor(), name: "LoggingInterceptor")

// Output:
// ๐Ÿš€ [A1B2C3D4] Entering PaymentService.processPayment
//    Parameters: ["amount": 100.0, "cardToken": "tok_..."]
// โœ… [A1B2C3D4] Completed PaymentService.processPayment in 45.23ms
//    Result: PaymentResult(id: "pay_123", status: "success")
```

##### PerformanceInterceptor

```swift
// Tracks execution times and identifies slow methods
InterceptorRegistry.register(interceptor: PerformanceInterceptor(), name: "PerformanceInterceptor")

// Get performance statistics
if let stats = PerformanceInterceptor.getStats(for: "PaymentService.processPayment") {
    print("Average: \(stats.avg)ms, Min: \(stats.min)ms, Max: \(stats.max)ms")
}

// Print comprehensive performance report
PerformanceInterceptor.printPerformanceReport()
```

#### ๐Ÿ”„ **Interceptor Registration**

Register your interceptors with the global registry:

```swift
// App startup - register all interceptors
InterceptorRegistry.registerDefaults()  // Registers built-in interceptors

// Register custom interceptors
InterceptorRegistry.register(
    interceptor: CustomSecurityInterceptor(),
    name: "SecurityInterceptor"
)
InterceptorRegistry.register(
    interceptor: CustomCacheInterceptor(),
    name: "CacheInterceptor"
)
```

#### ๐ŸŽฏ **Real-World Example: E-commerce Service**

```swift
class OrderService {
    @Interceptor(
        before: ["SecurityInterceptor", "ValidationInterceptor", "LoggingInterceptor"],
        after: ["InventoryInterceptor", "EmailInterceptor", "MetricsInterceptor"],
        onError: ["ErrorReportingInterceptor", "CompensationInterceptor"]
    )
    func createOrder(customerId: String, items: [OrderItem]) throws -> Order {
        // Pure business logic - all concerns handled by interceptors
        return try OrderProcessor.createOrder(customerId: customerId, items: items)
    }

    @Interceptor(before: ["CacheInterceptor"])
    func getOrderHistory(customerId: String) async -> [Order] {
        return await OrderRepository.findByCustomer(customerId)
    }
}
```

**Generated method calls:**

```swift
// The macro generates intercepted versions you can call explicitly
let order = orderService.createOrderIntercepted(customerId: "123", items: orderItems)

// Or use the original method - interceptors only run on the *Intercepted version
let order = orderService.createOrder(customerId: "123", items: orderItems)  // No interception
```

#### โšก **Performance Benefits**

- **Zero Overhead When Unused**: No interceptors = no performance impact
- **Compile-Time Validation**: Invalid interceptor references caught at build time
- **Minimal Runtime Cost**: Registry lookup + method calls only
- **Memory Efficient**: No reflection, no dynamic proxies
- **Thread Safe**: Built-in concurrent access to interceptor registry

๐Ÿ—๏ธ Complete Example: Real-World Application

Here's a complete example showing how all three macros work together in a real iOS application:

Domain Layer

import SwinjectMacros

// MARK: - Core Services

@Injectable(scope: .container)
class NetworkClient: APIClient {
    init() {
        // Network configuration
    }

    func fetchUser(id: String) async throws -> UserData {
        // Network implementation
    }
}

@Injectable(scope: .container)
class DatabaseManager: DatabaseService {
    init() {
        // Database setup
    }

    func save(_ user: UserData) async throws {
        // Database implementation
    }
}

@Injectable
class LoggerService {
    init() {
        // Logger setup
    }

    func log(_ message: String) {
        print("๐Ÿ“ฑ \(message)")
    }
}

// MARK: - Business Logic

@Injectable
class UserService {
    private let apiClient: APIClient
    private let database: DatabaseService
    private let logger: LoggerService

    init(apiClient: APIClient, database: DatabaseService, logger: LoggerService) {
        self.apiClient = apiClient
        self.database = database
        self.logger = logger
    }

    func getUser(id: String) async throws -> User {
        logger.log("Fetching user: \(id)")
        let userData = try await apiClient.fetchUser(id: id)
        try await database.save(userData)
        return User(from: userData)
    }
}

// MARK: - Factory Services (Need Runtime Parameters)

@AutoFactory
class UserSearchService {
    private let apiClient: APIClient    // Injected
    private let database: DatabaseService // Injected
    private let query: String           // Runtime parameter
    private let filters: [SearchFilter] // Runtime parameter

    init(apiClient: APIClient, database: DatabaseService,
         query: String, filters: [SearchFilter]) {
        self.apiClient = apiClient
        self.database = database
        self.query = query
        self.filters = filters
    }

    func search() async throws -> [User] {
        // Search implementation
        return []
    }
}

Application Setup

import Swinject
import SwinjectMacros

class AppAssembly: Assembly {
    func assemble(container: Container) {
        // All @Injectable services register themselves!
        NetworkClient.register(in: container)
        DatabaseManager.register(in: container)
        LoggerService.register(in: container)
        UserService.register(in: container)

        // Register factories for services with runtime parameters
        container.registerFactory(UserSearchServiceFactory.self)
    }
}

@main
struct MyApp: App {
    let container = Container()

    init() {
        let assembler = Assembler([AppAssembly()], container: container)
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(container.resolve(UserService.self)!)
        }
    }
}

SwiftUI Integration

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var userService: UserService
    @State private var searchQuery = ""
    @State private var users: [User] = []

    // Inject the factory for services with runtime parameters
    private let searchFactory: UserSearchServiceFactory

    init(searchFactory: UserSearchServiceFactory = Container.shared.resolve(UserSearchServiceFactory.self)!) {
        self.searchFactory = searchFactory
    }

    var body: some View {
        NavigationView {
            VStack {
                SearchBar(text: $searchQuery, onSearchButtonClicked: performSearch)

                List(users, id: \.id) { user in
                    UserRow(user: user)
                }
            }
            .navigationTitle("Users")
        }
    }

    private func performSearch() {
        Task {
            let searchService = searchFactory.makeUserSearchService(
                query: searchQuery,
                filters: [.active, .verified]
            )

            do {
                users = try await searchService.search()
            } catch {
                print("Search failed: \(error)")
            }
        }
    }
}

Testing

import XCTest
@testable import MyApp

@TestContainer
class UserServiceTests: XCTestCase {
    var container: Container!

    // Service properties automatically detected
    var apiClient: APIClient!
    var database: DatabaseService!
    var logger: LoggerService!

    var userService: UserService!

    override func setUp() {
        super.setUp()

        // Generated method creates container with mocks
        container = setupTestContainer()

        // Resolve mocked dependencies
        apiClient = container.resolve(APIClient.self)!
        database = container.resolve(DatabaseService.self)!
        logger = container.resolve(LoggerService.self)!

        // Your service under test gets the mocks automatically
        UserService.register(in: container)
        userService = container.resolve(UserService.self)!
    }

    func testGetUser() async throws {
        // Setup mock behavior
        let mockAPI = apiClient as! MockAPIClient
        mockAPI.fetchUserResult = UserData(id: "123", name: "John Doe")

        // Test your service
        let user = try await userService.getUser(id: "123")

        // Verify behavior
        XCTAssertEqual(user.name, "John Doe")
        XCTAssertTrue(mockAPI.fetchUserCalled)

        let mockDB = database as! MockDatabaseService
        XCTAssertTrue(mockDB.saveCalled)
    }
}

๐ŸŽฏ Best Practices

1. Service Design Guidelines

// โœ… GOOD: Clear service boundaries
@Injectable
class UserAuthenticationService {
    private let apiClient: APIClient
    private let tokenStorage: TokenStorage

    init(apiClient: APIClient, tokenStorage: TokenStorage) {
        self.apiClient = apiClient
        self.tokenStorage = tokenStorage
    }
}

// โŒ AVOID: Too many dependencies (code smell)
@Injectable
class GodService {
    // 15+ dependencies - consider breaking this down
    init(dep1: Dep1, dep2: Dep2, /* ... */, dep15: Dep15) { }
}

2. Scope Selection

// Use .container for expensive resources
@Injectable(scope: .container)
class DatabaseConnection { }

// Use .graph (default) for business logic
@Injectable  // scope: .graph is default
class UserService { }

// Use .singleton sparingly for app-wide state
@Injectable(scope: .singleton)
class AppConfiguration { }

3. Factory vs Injectable Decision

// โœ… Use @Injectable for pure services
@Injectable
class EmailService {
    init(smtpClient: SMTPClient) { }
}

// โœ… Use @AutoFactory for services needing runtime data
@AutoFactory
class EmailComposer {
    init(emailService: EmailService, recipient: String, subject: String) { }
}

4. Testing Strategy

// โœ… GOOD: Focused test setup
@TestContainer
class UserServiceTests: XCTestCase {
    var apiClient: APIClient!
    var database: DatabaseService!
    // Only dependencies you need
}

// โœ… GOOD: Custom mocks when needed
@TestContainer(autoMock: false)
class ComplexServiceTests: XCTestCase {
    override func setUp() {
        super.setUp()
        container = setupTestContainer()

        // Use sophisticated mocks
        registerAPIClient(mock: RecordingMockAPIClient())
    }
}

โš ๏ธ Common Pitfalls & Solutions

1. Circular Dependencies

// โŒ PROBLEM: Circular dependency
@Injectable
class ServiceA {
    init(serviceB: ServiceB) { }
}

@Injectable
class ServiceB {
    init(serviceA: ServiceA) { }  // Circular!
}

// โœ… SOLUTION: Break the cycle with protocols or refactoring
protocol ServiceAProtocol { }

@Injectable
class ServiceA: ServiceAProtocol {
    init(serviceB: ServiceB) { }
}

@Injectable
class ServiceB {
    init(serviceA: ServiceAProtocol) { }  // Now uses protocol
}

2. Runtime Parameters in @Injectable

// โŒ PROBLEM: Runtime parameters in @Injectable
@Injectable  // โš ๏ธ Compiler warning
class ReportService {
    init(database: DatabaseService, reportType: String) { }
    //                                ^^^^^^^^^^^ Runtime parameter!
}

// โœ… SOLUTION: Use @AutoFactory instead
@AutoFactory
class ReportService {
    init(database: DatabaseService, reportType: String) { }
}

3. Missing Protocol Registrations

// โŒ PROBLEM: Concrete type but need protocol
@Injectable
class ConcreteAPIClient: APIClient {
    init() { }
}

// Later...
let client = container.resolve(APIClient.self)  // nil! Not registered

// โœ… SOLUTION: Register both concrete and protocol
class AppAssembly: Assembly {
    func assemble(container: Container) {
        ConcreteAPIClient.register(in: container)

        // Also register the protocol
        container.register(APIClient.self) { resolver in
            resolver.resolve(ConcreteAPIClient.self)!
        }
    }
}

๐Ÿ”ฎ Roadmap

SwinjectMacros is actively developed with 25+ macros planned. Here's what's coming:

โœ… Phase 1: Complete (Current)

  • @Injectable - Service registration
  • @AutoFactory - Factory pattern generation
  • @TestContainer - Test mock setup

๐Ÿšง Phase 2: AOP & Interceptors (Next)

  • @Interceptor - Method interception with before/after/onError hooks
  • @PerformanceTracked - Automatic performance monitoring
  • @Retry - Automatic retry logic with backoff strategies
  • @CircuitBreaker - Circuit breaker pattern implementation

๐Ÿ“‹ Phase 3: Advanced DI Patterns

  • @LazyInject - Lazy dependency resolution
  • @WeakInject - Weak reference injection
  • @AsyncInject - Async dependency initialization
  • @OptionalInject - Optional dependency handling
  • @NamedInject - Named dependency injection

๐Ÿงช Phase 4: Testing & Validation

  • @Spy - Automatic spy generation
  • @MockResponse - HTTP response mocking
  • @StubService - Service stubbing utilities
  • @ValidatedContainer - Container validation at compile-time

โš™๏ธ Phase 5: Configuration & Features

  • @FeatureToggle - Feature flag integration
  • @ConfigurableService - Configuration-driven services
  • @ConditionalRegistration - Conditional service registration
  • @EnvironmentService - Environment-specific implementations

๐Ÿ”ง Phase 6: SwiftUI & Combine

  • @EnvironmentInject - SwiftUI Environment integration
  • @ViewModelInject - MVVM pattern support
  • @InjectedStateObject - State management integration
  • @PublisherInject - Combine publishers injection

โš ๏ธ Known Issues

1. ObjectScope.module Compatibility

Issue: The custom .module ObjectScope for Swinject is currently disabled due to version compatibility issues.

Impact:

  • Tests using .inObjectScope(.module) are temporarily disabled
  • Module-scoped services fall back to standard Swinject scopes

Workaround: Use .container or .graph scopes until compatibility is resolved.

Status: Under investigation - related to Swinject's internal ObjectScope initializer accessibility.

2. Performance Test Stability

Issue: Some performance and stress tests may fail in development environments due to:

  • Concurrent container access without proper synchronization
  • High resource usage during test execution
  • Timing-sensitive assertions

Impact:

  • Performance benchmark tests may show failures
  • Stress tests with multiple threads may encounter race conditions

Workaround:

  • Run performance tests individually for more stable results
  • Use Container.shared.synchronizedResolve() for thread-safe resolution
  • Consider running performance tests in release builds

Status: Known limitation - tests work correctly in production scenarios.

3. Macro Expansion Test Maintenance

Issue: Macro implementation updates may cause test failures in API design validation tests.

Impact:

  • Some macro expansion tests temporarily disabled pending updates
  • Generated code format changes require test expectation updates

Workaround: Tests are disabled until expected outputs can be updated to match current macro implementations.

Status: In progress - tests will be re-enabled with updated expectations.

4. Build Warnings from Dependencies

Issue: Swinject dependency generates warnings about unhandled files:

warning: 'swinject': found 5 file(s) which are unhandled
    Sources/Container.Arguments.erb
    Sources/PrivacyInfo.xcprivacy
    ...

Impact: Cosmetic build warnings that don't affect functionality.

Workaround: These warnings can be safely ignored - they're from Swinject's template files.

Status: External dependency issue - no action required.

5. Module System Dependency Resolution

Issue: Complex module dependency chains may fail to resolve services properly during initialization.

Impact:

  • testModuleDependencies temporarily disabled
  • Advanced module system features may need additional setup

Workaround:

  • Register modules in dependency order (dependencies first)
  • Use explicit dependency declarations in module protocols
  • Consider simpler dependency graphs during development

Status: Under active development as part of Phase 2 module system improvements.


Note: These issues don't affect core functionality (@Injectable, @AutoFactory, @TestContainer) which work reliably in production. They primarily impact advanced features and test scenarios.

๐Ÿค Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Setup

git clone https://github.com/brunogama/SwinjectMacros.git
cd SwinjectMacros
swift build
swift test

๐Ÿ“„ License

SwinjectMacros is released under the MIT License. See LICENSE for details.

๐Ÿ™ Acknowledgments

  • Built on top of the excellent Swinject framework
  • Powered by Swift Macros introduced in Swift 5.9
  • Inspired by dependency injection frameworks from other ecosystems

Ready to eliminate dependency injection boilerplate? Get started with SwinjectMacros today! ๐Ÿš€

Package Metadata

Repository: brunogama/SwinjectMacros

Stars: 4

Forks: 0

Open issues: 1

Default branch: main

Primary language: swift

License: MIT

Topics: ios, swift, swift-macros, swinject

README: README.md