Production-grade Swift dependency injection framework with compile-time safety and zero external dependencies.
- ✅ Compile-time safe dependency injection via Swift macros
- ✅ Zero external dependencies (only Swift standard library)
- ✅ Context-aware resolution (live/test/preview auto-detection)
- ✅ TaskLocal-based scoping for structured concurrency safety
- ✅ Multi-platform support (iOS 16+, macOS 13+, tvOS 16+, watchOS 9+, visionOS 1+)
- ✅ Lifetime management (transient, cached, singleton)
- ✅ Async/await first with async dependency support
- ✅ Property wrapper DSL for declarative injection
- ✅ Thread-safe singleton caching
- ✅ Diagnostics for debugging dependency configuration
Add SimpleDependencyManager to your Package.swift:
dependencies: [
.package(url: "https://github.com/your-org/SimpleDependencyManager.git", from: "1.0.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["SimpleDependencyManager"]
)
]import SimpleDependencyManager
@Dependency
struct NetworkService: Sendable {
var fetchData: @Sendable () async throws -> Data
static var liveValue: NetworkService {
NetworkService(
fetchData: {
let url = URL(string: "https://api.example.com")!
return try await URLSession.shared.data(from: url).0
}
)
}
static var testValue: NetworkService {
NetworkService(
fetchData: { Data("test".utf8) }
)
}
}import SimpleDependencyManager
struct ContentView: View {
@Injected(\.networkService) var network
var body: some View {
Button("Fetch") {
Task {
let data = try await network.fetchData()
print("Received: \(data)")
}
}
}
}import Testing
@testable import SimpleDependencyManager
@Test func testNetworkFetch() async throws {
let data = try await withDependencies {
$0.networkService = NetworkService.testValue
} operation: {
try await DI[\.networkService].fetchData()
}
#expect(data == Data("test".utf8))
}Dependencies automatically resolve to the correct implementation based on environment:
| Context | Detection Method | Use Case |
|---|---|---|
| Live | Default | Production app |
| Test | XCTestConfigurationFilePath env var |
Unit tests |
| Preview | XCODE_RUNNING_FOR_PREVIEWS env var |
SwiftUI previews |
// No configuration needed - auto-detects!
let service = DI[\.myService] // Live in app, test in tests, preview in previewsControl how long dependency instances live:
@Dependency
struct DatabaseService {
// One instance globally (thread-safe)
static var lifetime: DependencyLifetime { .singleton }
}
@Dependency
struct RequestHandler {
// New instance every access
static var lifetime: DependencyLifetime { .transient }
}
@Dependency
struct CacheService {
// Default: one instance per scope
static var lifetime: DependencyLifetime { .cached }
}Override dependencies for specific scopes without affecting other code:
// Outer scope: uses live dependencies
await performOperation()
// Inner scope: uses mock
await withDependencies {
$0.networkService = MockNetwork()
} operation: {
await performOperation() // Uses mock
}
// Outer scope again: back to live
await performOperation()TaskLocal Isolation: Child tasks automatically inherit parent scope, while sibling tasks remain isolated.
For dependencies requiring async initialization:
@Dependency
struct DatabaseService: AsyncDependencyKey {
static func resolve() async throws -> DatabaseService {
let db = try await Database.connect()
return DatabaseService(database: db)
}
}
// Warmup at app launch
@main
struct MyApp: App {
init() {
Task {
await warmupDependencies([DatabaseService.self])
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Use phantom types for variant selection:
struct Live: DependencyTag {}
struct Sandbox: DependencyTag {}
@Dependency
struct APIClient {
static var liveValue: TaggedDependencies<APIClient> {
TaggedDependencies(
Live.self: APIClient(baseURL: "https://api.prod.com"),
Sandbox.self: APIClient(baseURL: "https://api.sandbox.com")
)
}
}
// Access specific variant
let prodClient = DI[\.apiClient][Live.self]
let sandboxClient = DI[\.apiClient][Sandbox.self]Generate memberwise initializers for test doubles:
@DependencyClient
struct MockNetworkService: Sendable {
var fetchData: @Sendable () async throws -> Data
var saveData: @Sendable (Data) async throws -> Void
}
// In tests
@Test func handlesNetworkError() async {
let mock = MockNetworkService(
fetchData: { throw NetworkError.timeout }
)
await withDependencies {
$0.networkService = mock
} operation: {
await #expect(throws: NetworkError.timeout) {
try await performNetworkOperation()
}
}
}Debug dependency configuration:
// Get snapshot of current state
let snapshot = DependencyDiagnostics.snapshot()
print(snapshot.registeredKeys) // All configured dependencies
// Assert dependency configured in tests
#expect(throws: Never.self) {
DI.assertDependencyConfigured(\.myService)
}
// Warn if using default value
DI.warnIfDefaultValue(\.myService)SimpleDependencyManager uses a hybrid architecture combining:
- TaskLocal-based scoping - Automatic propagation through structured concurrency
- Protocol-oriented design -
DependencyKeyprotocol defines contracts - Code generation - Swift macros eliminate boilerplate at compile-time
- Property wrapper pattern -
@Injectedfor declarative injection
Key guarantees:
- No global mutable state - All state is TaskLocal-scoped
- Thread-safe - Singleton cache uses
NSLock - Type-safe - Compile-time enforcement via protocols
- Sendable - All dependencies must conform to
Sendable
- Quick Start - Get running in 5 minutes
- Onboarding Guide - Comprehensive contributor guide (90+ pages)
- Test Constitution - Testing rules and pyramid
- OpenSpec Workflow - Spec-driven development
- Migration Examples - Real-world usage patterns
- Swift: 6.0+
- Platforms: iOS 16.0+ / macOS 13.0+ / tvOS 16.0+ / watchOS 9.0+ / visionOS 1.0+
- Xcode: 16.0+
- Dependencies: None (only Swift standard library)
We welcome contributions! Please read our Contributing Guide first.
# Install tools
brew install swift-format swiftlint xcbeautify
pip3 install pre-commit
# Clone and setup
git clone <repository-url>
cd SimpleDependencyManager
pre-commit install
git config commit.template .gitmessage
# Verify
swift build
swift test- Zero warnings tolerated - Warnings treated as errors
- 100% test coverage target - All changes require tests
- Google Swift Style - Enforced via swift-format and SwiftLint
- Conventional Commits - Standardized commit messages
- Pre-commit hooks - Auto-format, lint, and test
See ONBOARDING.md for complete contributor guide.
@Dependency
struct AuthService: Sendable {
var login: @Sendable (String, String) async throws -> Token
var logout: @Sendable () async -> Void
var currentUser: @Sendable () async -> User?
static var liveValue: AuthService {
AuthService(
login: { username, password in
try await APIClient.shared.login(username, password)
},
logout: {
await APIClient.shared.logout()
},
currentUser: {
await UserDefaults.standard.currentUser
}
)
}
static var testValue: AuthService {
AuthService(
login: { _, _ in Token.mock },
logout: { },
currentUser: { User.mock }
)
}
}struct LoginView: View {
@Injected(\.authService) var auth
@State private var username = ""
@State private var password = ""
var body: some View {
VStack {
TextField("Username", text: $username)
SecureField("Password", text: $password)
Button("Login") {
Task {
try await auth.login(username, password)
}
}
}
}
}@Test func loginSucceeds() async throws {
let mockAuth = AuthService(
login: { _, _ in Token(value: "test-token") }
)
let token = try await withDependencies {
$0.authService = mockAuth
} operation: {
try await DI[\.authService].login("user", "pass")
}
#expect(token.value == "test-token")
}| Feature | SimpleDependencyManager | Swinject | Resolver | Factory |
|---|---|---|---|---|
| Compile-time safety | ✅ | ❌ | ❌ | ✅ |
| Zero dependencies | ✅ | ❌ | ❌ | ✅ |
| TaskLocal scoping | ✅ | ❌ | ❌ | ❌ |
| Auto context detection | ✅ | ❌ | ❌ | ❌ |
| Swift macros | ✅ | ❌ | ❌ | ❌ |
| Property wrappers | ✅ | ❌ | ✅ | ✅ |
| Async/await first | ✅ | ❌ | ❌ | ✅ |
SimpleDependencyManager is released under the MIT license. See LICENSE for details.
Built with:
- swift-syntax - Swift AST manipulation for macros
- Inspired by SwiftUI's
@Environmentpattern - Following Google Swift Style Guide
- Documentation: docs/
- Issues: Open an issue for bugs or feature requests
- Discussions: Use GitHub Discussions for questions
Happy dependency injecting! 🚀