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-timeKey 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:
testModuleDependenciestemporarily 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