Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

CoolDependencyManager

A lightweight, type-safe dependency injection container for Swift. Supports keep-alive (singleton), transient (fresh instance), and flow-scoped lifetimes with automatic dependency graph resolution and circular dependency detection.

Version: 0.1.2 Last updated (UTC): 2026-06-08T13:57:54Z

Release metadata lives in VERSION, LAST_UPDATED, and CoolDependencyManagerMetadata.swift. Enable automatic patch-version and timestamp updates for commits made on main:

./scripts/install-git-hooks.sh

Installation

Swift Package Manager

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/darkbringer1/CoolDependencyManager.git", branch: "main")
]

Then add "CoolDependencyManager" to your target's dependencies:

.target(
    name: "MyApp",
    dependencies: [
        .product(name: "CoolDependencyManager", package: "CoolDependencyManager"),
    ]
)

Or in Xcode: File → Add Package Dependencies → paste the repository URL.

Core Concepts

Dependency Lifetimes

Lifetime Behavior Use Case
.keepAlive Created once, cached forever (singleton) Managers, services, shared state
.transient New instance on every get() call Stateful objects, request-scoped work
.flow Cached within a DependencyScope, released when scope ends Multi-step flows (e.g., registration, checkout)

Components

  • DependencyContainer — The central registry. Stores factories, resolves dependencies, manages caches.
  • DependencyKey<T> — Identifies a dependency by type and lifetime.
  • DependencyFactory — Protocol for creating dependency instances. Concrete types: SimpleDependencyFactory, TypedDependencyFactory, TypedDependencyFactory2.
  • DependencyScope — A named cache boundary for flow-scoped dependencies.
  • DependencyGraph — Internal topological sort and cycle detection engine.

Usage

1. Define Your Protocols and Types

protocol NetworkServiceProtocol {
    func fetch(url: URL) async throws -> Data
}

class NetworkService: NetworkServiceProtocol {
    func fetch(url: URL) async throws -> Data {
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }
}

protocol AuthManagerProtocol {
    var isLoggedIn: Bool { get }
}

class AuthManager: AuthManagerProtocol {
    private let networkService: NetworkServiceProtocol
    var isLoggedIn: Bool = false

    init(networkService: NetworkServiceProtocol) {
        self.networkService = networkService
    }
}

2. Define Dependency Keys

Extend DependencyContainer with computed properties that return DependencyKey values. This gives you type-safe KeyPath-based injection.

import CoolDependencyManager

extension DependencyContainer {
    var networkService: DependencyKey<NetworkServiceProtocol> {
        DependencyKey<NetworkServiceProtocol>(keepAlive: true) // singleton
    }

    var authManager: DependencyKey<AuthManagerProtocol> {
        DependencyKey<AuthManagerProtocol>(keepAlive: true) // singleton
    }

    var sessionToken: DependencyKey<String> {
        DependencyKey<String>(name: "sessionToken", lifetime: .transient) // fresh each time
    }
}

3. Register Factories

Register factories that tell the container how to create each dependency.

func registerDependencies() {
    let container = DependencyContainer.shared

    // Simple factory — no dependencies needed
    let networkFactory = SimpleDependencyFactory(
        key: container.networkService
    ) { _ in
        NetworkService()
    }
    try? container.registerFactory(networkFactory)

    // Typed factory — declares dependency on NetworkServiceProtocol
    let authFactory = TypedDependencyFactory(
        key: container.authManager,
        dependency1: container.networkService
    ) { networkService in
        AuthManager(networkService: networkService)
    }
    try? container.registerFactory(authFactory)

    // Transient factory — new UUID string each time
    let tokenFactory = SimpleDependencyFactory(
        key: container.sessionToken
    ) { _ in
        UUID().uuidString
    }
    try? container.registerFactory(tokenFactory)
}

4. Build the Container

After registering all factories, build the container. This creates all keep-alive dependencies in topological order (respecting inter-dependencies). Transient dependencies are not created during build — they're created on-demand.

func setupApp() {
    registerDependencies()
    try? DependencyContainer.shared.buildAll()
}

5. Resolve Dependencies

Using Property Wrappers (Recommended)

class ProfileViewModel {
    @Dependency(\.authManager) var authManager
    @Dependency(\.networkService) var networkService

    func loadProfile() {
        guard authManager.isLoggedIn else { return }
        // use networkService...
    }
}

Using @Observable

The Observation macro turns tracked stored properties into computed properties. Exclude dependency wrappers from observation so @Dependency remains a property wrapper:

import Observation

@MainActor
@Observable
final class ProfileViewModel {
    @ObservationIgnored
    @Dependency(\.authManager)
    var authManager
}

@ObservationIgnored is appropriate because changing the wrapper itself is not observable state; if the resolved service is observable, SwiftUI tracks that service when its properties are read.

For initializer injection, use DependencyContainer.shared.resolve(\.authManager). Use tryResolve(_:) when the caller should handle resolution errors instead of failing fast.

Using get() Directly

let container = DependencyContainer.shared
let auth = try container.get(container.authManager)
let token = try container.get(container.sessionToken) // fresh each call

Advanced Usage

Scoped Dependencies (Flow Lifetime)

Scoped dependencies are cached for the duration of a DependencyScope. When the scope is released, all cached instances within it are discarded. This is ideal for multi-step flows like registration or checkout.

extension DependencyContainer {
    var registrationData: DependencyKey<RegistrationData> {
        DependencyKey<RegistrationData>(lifetime: .flow)
    }
}

Manual Scope Management

let container = DependencyContainer.shared

// Create a scope
let scope = container.makeScope()

// Resolve within scope — same instance returned for same scope
let data1 = try container.get(container.registrationData, scope: scope)
let data2 = try container.get(container.registrationData, scope: scope)
// data1 === data2 ✅

// Different scope → different instance
let otherScope = container.makeScope()
let data3 = try container.get(container.registrationData, scope: otherScope)
// data1 !== data3 ✅

// Release scope when flow ends
container.releaseScope(scope.id)

Scope Stack (Implicit Flow Scope)

Push/pop scopes onto a stack so .flow dependencies resolve automatically without passing a scope around:

let container = DependencyContainer.shared
let scope = container.makeScope()

container.pushScope(scope)

// .flow dependencies now resolve against the pushed scope
let data = try container.get(container.registrationData)

container.popScope(scope)

withScope Convenience

try container.withScope { scope in
    let data = try container.get(container.registrationData)
    // scope is auto-pushed and popped
}

@ScopedDependency Property Wrapper

class RegistrationStepViewModel {
    @ScopedDependency(.shared, scope: { myScope }, \.registrationData)
    var registrationData: RegistrationData
}

Container-Specific Injection

Use @ContainerDependency when you have multiple container instances (e.g., production vs. test):

class MyViewController {
    @ContainerDependency(DependencyContainer.shared, \.networkService)
    var networkService: NetworkServiceProtocol
}

Lazy Dependency Resolution

Dependencies resolved only on first access:

class ExpensiveFeatureViewModel {
    @LazyDependency(DependencyContainer.shared, \.heavyService)
    var heavyService: HeavyServiceProtocol
}

Typed Factories with Multiple Dependencies

For factories that need 2 resolved dependencies:

let factory = TypedDependencyFactory2(
    key: container.checkoutService,
    dependency1: container.networkService,
    dependency2: container.authManager
) { network, auth in
    CheckoutService(network: network, auth: auth)
}
try container.registerFactory(factory)

Overriding Registrations

Replace an existing factory (useful for feature flags or runtime swaps):

let newFactory = SimpleDependencyFactory(key: container.networkService) { _ in
    MockNetworkService()
}
try container.registerFactory(newFactory, override: true)

Testing

Mock Injection

Use registerMock(_:_:) to bypass factories and inject a pre-built instance directly:

@Suite(.serialized)
@MainActor
struct MyServiceTests {
    init() {
        DependencyContainer.shared.reset()
    }

    @Test func fetchProfile() throws {
        let container = DependencyContainer.shared
        let mockNetwork = MockNetworkService()

        container.registerMock(container.networkService, mockNetwork)

        let viewModel = ProfileViewModel()
        // viewModel.networkService is now mockNetwork
    }
}

Full Reset

reset() clears all factories, cached instances, scopes, and the dependency graph:

DependencyContainer.shared.reset()

Error Handling

All resolution errors are thrown as DependencyContainerError:

Error Cause
.productNotFound(name:) No factory registered for the requested key
.factoryAlreadyRegistered(name:) Duplicate registration without override: true
.circularDependencyDetected(cycles:) A → B → A dependency cycle found during build
.dependencyNotRegistered(name:requiredBy:) A factory's declared dependency hasn't been registered
.dependencyTypeMismatch(expected:found:) Resolved type doesn't match the expected type in a typed factory

Thread Safety

The container is @MainActor-isolated. All public APIs must be called from the main thread. The container conforms to @unchecked Sendable for storage in contexts that require Sendable, but access is only safe from @MainActor.

Requirements

  • Swift 6.0+
  • iOS 15+ / macOS 12+

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages