Advanced Dependency Injection Utilities for Swift using Compile-Time Macros
SwinjectMacros brings the power of Swift Macros to dependency injection, dramatically reducing boilerplate code while maintaining type safety and performance. Built on top of the proven Swinject framework, it provides 25+ compile-time macros for modern Swift applications.
Traditional dependency injection in Swift requires significant boilerplate code:
// 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)
}
}// 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- 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
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"]
)
]- Swift 5.9+ (Required for macro support)
- iOS 15.0+ / macOS 12.0+ / watchOS 8.0+ / tvOS 15.0+
- Xcode 15.0+
The @Injectable macro automatically generates dependency injection registration code for your services.
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.
The macro examines your class/struct initializer and:
- Identifies service dependencies (types ending in Service, Repository, Client, etc.)
- Generates resolver calls for each dependency
- Creates a static
register(in:)method - Adds
Injectableprotocol conformance
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!):
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)
}
}Control the lifecycle of your services:
@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 */ }
}Register multiple implementations of the same protocol:
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")Handle optional dependencies gracefully:
@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 requiredThe 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 | @AutoFactory |
String = "default" |
Configuration Parameter | Use default value |
The @AutoFactory macro generates factory protocols and implementations for services that need runtime parameters.
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):
// 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):
@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
}
}The macro analyzes your initializer and:
- Separates injected dependencies from runtime parameters
- Generates a factory protocol with a
makemethod for runtime parameters only - Generates a factory implementation that resolves dependencies and accepts runtime parameters
- Handles async/throws automatically
@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:
// 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
)
}
}@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@AutoFactory(name: "CustomReportFactory")
class ReportService {
init(database: DatabaseService, reportId: String) { /* ... */ }
}
// Generates: protocol CustomReportFactory { ... }
// Instead of: protocol ReportServiceFactory { ... }// 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()
}
}
}The @TestContainer macro automatically generates test container setup with mocks for your test classes.
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):
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):
@TestContainer
class UserServiceTests: XCTestCase {
var apiClient: APIClient!
var database: DatabaseService!
var logger: LoggerService!
// Container setup is automatically generated!
}The macro scans your test class properties and:
- Identifies service properties (types ending in Service, Repository, Client, etc.)
- Generates a
setupTestContainer()method that creates and configures a container - Generates mock registration helpers for each service type
- Supports custom mock prefixes and scopes
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:
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)
}
}@TestContainer(mockPrefix: "Stub")
class UserServiceTests: XCTestCase {
var apiClient: APIClient!
var database: DatabaseService!
}
// Generates: StubAPIClient(), StubDatabaseService()
// Instead of: MockAPIClient(), MockDatabaseService()@TestContainer(scope: .container)
class UserServiceTests: XCTestCase {
var database: DatabaseService! // Will be registered as singleton
}@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)!
}
}@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")
}
}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.
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):
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
}
}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)
}
}The @Interceptor macro generates an intercepted version of your method that:
- Creates rich context with method name, parameters, types, and execution metadata
- Executes before interceptors in specified order for setup/validation
- Calls your original method with full error handling
- Executes after interceptors in reverse order (LIFO) for cleanup
- Handles errors with dedicated error interceptors
- Provides performance metrics with execution timing
// 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)
}// 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()
}class UtilityService {
@Interceptor(before: ["LoggingInterceptor"])
static func validateInput(data: String) -> Bool {
return InputValidator.validate(data)
}
}All interceptors must conform to the MethodInterceptor protocol:
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
}
}SwinjectMacros provides several production-ready interceptors:
// 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")// 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()Register your interceptors with the global registry:
// 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"
)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:
// 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- 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
Here's a complete example showing how all three macros work together in a real iOS application:
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 []
}
}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)!)
}
}
}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)")
}
}
}
}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)
}
}// โ
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) { }
}// 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 { }// โ
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) { }
}// โ
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())
}
}// โ 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
}// โ 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) { }
}// โ 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)!
}
}
}SwinjectMacros is actively developed with 25+ macros planned. Here's what's coming:
@Injectable- Service registration@AutoFactory- Factory pattern generation@TestContainer- Test mock setup
@Interceptor- Method interception with before/after/onError hooks@PerformanceTracked- Automatic performance monitoring@Retry- Automatic retry logic with backoff strategies@CircuitBreaker- Circuit breaker pattern implementation
@LazyInject- Lazy dependency resolution@WeakInject- Weak reference injection@AsyncInject- Async dependency initialization@OptionalInject- Optional dependency handling@NamedInject- Named dependency injection
@Spy- Automatic spy generation@MockResponse- HTTP response mocking@StubService- Service stubbing utilities@ValidatedContainer- Container validation at compile-time
@FeatureToggle- Feature flag integration@ConfigurableService- Configuration-driven services@ConditionalRegistration- Conditional service registration@EnvironmentService- Environment-specific implementations
@EnvironmentInject- SwiftUI Environment integration@ViewModelInject- MVVM pattern support@InjectedStateObject- State management integration@PublisherInject- Combine publishers injection
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.
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.
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.
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.
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.
We welcome contributions! Please see our Contributing Guide for details.
git clone https://github.com/brunogama/SwinjectMacros.git
cd SwinjectMacros
swift build
swift testSwinjectMacros is released under the MIT License. See LICENSE for details.
- 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! ๐