Skip to content

brunogama/SimpleDependencyManager

Repository files navigation

SimpleDependencyManager

Production-grade Swift dependency injection framework with compile-time safety and zero external dependencies.

Swift 6.0+ Platforms License Swift Package Manager


Features

  • 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

Quick Start

Installation

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"]
    )
]

Basic Usage

1. Define a Dependency

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) }
        )
    }
}

2. Use the Dependency

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)")
            }
        }
    }
}

3. Test with Overrides

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))
}

Core Concepts

Context-Aware Resolution

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 previews

Lifetime Management

Control 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 }
}

Scoped Overrides

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.


Advanced Features

Async Dependencies

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()
        }
    }
}

Tagged Dependencies

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]

Test Doubles with @DependencyClient

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()
        }
    }
}

Diagnostics

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)

Architecture

SimpleDependencyManager uses a hybrid architecture combining:

  1. TaskLocal-based scoping - Automatic propagation through structured concurrency
  2. Protocol-oriented design - DependencyKey protocol defines contracts
  3. Code generation - Swift macros eliminate boilerplate at compile-time
  4. Property wrapper pattern - @Injected for 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

Documentation


Requirements

  • 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)

Contributing

We welcome contributions! Please read our Contributing Guide first.

Quick Setup

# 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

Code Quality

  • 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.


Examples

Real-World Dependency

@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 }
        )
    }
}

Usage in SwiftUI

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)
                }
            }
        }
    }
}

Testing

@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")
}

Comparison with Other Solutions

Feature SimpleDependencyManager Swinject Resolver Factory
Compile-time safety
Zero dependencies
TaskLocal scoping
Auto context detection
Swift macros
Property wrappers
Async/await first

License

SimpleDependencyManager is released under the MIT license. See LICENSE for details.


Acknowledgments

Built with:


Support

  • Documentation: docs/
  • Issues: Open an issue for bugs or feature requests
  • Discussions: Use GitHub Discussions for questions

Happy dependency injecting! 🚀

About

Production-grade Swift dependency injection framework with compile-time safety and zero external dependencies

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors