Skip to content

Repository files navigation

TinyDependency

Swift Platforms Swift Package Manager License

中文文档

A lightweight dependency injection library for Swift, designed for non-view scenarios in SwiftUI projects.

Features

  • Familiar API: SwiftUI.Environment-like declarative syntax
  • Concurrency Safe: Sendable constraint enforced, @TaskLocal based
  • Three Scopes: Automatic switching between default, test, and preview
  • Lightweight: Zero dependencies, pure Swift (~200 lines of code)
  • Swift 6: Full support for strict concurrency checking

Requirements

  • Swift 6.0+
  • iOS 15+ / macOS 12+ / tvOS 15+ / watchOS 8+ / visionOS 1+

Installation

Swift Package Manager

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/fatbobman/TinyDependency.git", from: "0.3.0")
]

Or add via Xcode: File → Add Package Dependencies

Usage

1. Define Dependency Protocol

protocol Logger: Sendable {
    func log(_ message: String)
}

struct ProductionLogger: Logger {
    func log(_ message: String) {
        print("[\(Date())] \(message)")
    }
}

struct MockLogger: Logger {
    func log(_ message: String) {
        // Silent or log to memory
    }
}

2. Create Dependency Key

import TinyDependency

private struct LoggerKey: DependencyKey {
    static let defaultValue: Logger = ProductionLogger()
    static let testValue: Logger = MockLogger()
    static let previewValue: Logger = MockLogger()
}

extension DependencyValues {
    var logger: Logger {
        get { self[LoggerKey.self] }
        set { self[LoggerKey.self] = newValue }
    }
}

3. Use Dependencies

class UserService {
    @Dependency(\.logger) var logger

    func createUser(name: String) async {
        logger.log("Creating user: \(name)")
        // Business logic...
    }
}

4. Override in Tests

import Testing
@testable import YourApp

@Test
func testUserService() async {
    let customLogger = CustomMockLogger()

    await withDependencies {
        $0.logger = customLogger
    } operation: {
        let service = UserService()
        await service.createUser(name: "Alice")

        #expect(customLogger.messages.contains("Creating user: Alice"))
    }
}

Scope Explanation

TinyDependency automatically selects appropriate default values based on the environment:

Environment Value Used
App Runtime defaultValue
Test Environment testValue
Xcode Preview previewValue

Regular app runs use defaultValue in both Debug and Release builds. testValue is only selected when the process is actually running in a test environment.

You can also override the automatic detection by setting TINY_DEPENDENCY_CONTEXT to default, test, or preview. The value live is also accepted as an alias for default.

This is especially useful for integration checks that run outside the test runner, such as subprocess-based verification, command-line tools, or custom CI scripts. For example, you can force testValue in a normal process with:

env TINY_DEPENDENCY_CONTEXT=test swift run MyAppProbe

Supported values:

  • default
  • live (alias of default)
  • test
  • preview

Unknown values are ignored and the library falls back to automatic detection.

You can omit testValue and previewValue in DependencyKey - they will fall back to defaultValue:

private struct LoggerKey: DependencyKey {
    static let defaultValue: Logger = ProductionLogger()
    // testValue and previewValue automatically use defaultValue
}

Advanced Usage

Nested Scopes

withDependencies {
    $0.logger = Logger1()
} operation: {
    // Uses Logger1

    withDependencies {
        $0.logger = Logger2()
    } operation: {
        // Uses Logger2
    }

    // Back to Logger1
}

Async Support

await withDependencies {
    $0.database = MockDatabase()
} operation: {
    await someAsyncWork()
}

Concurrency Safety

Based on @TaskLocal, dependencies are automatically isolated between tasks:

await withTaskGroup(of: Void.self) { group in
    group.addTask {
        await withDependencies {
            $0.logger = Logger1()
        } operation: {
            // Task 1 uses Logger1
        }
    }

    group.addTask {
        await withDependencies {
            $0.logger = Logger2()
        } operation: {
            // Task 2 uses Logger2, unaffected by Task 1
        }
    }
}

Design Philosophy

Why Require Sendable?

Enforcing Sendable constraint simplifies concurrency safety design:

  • No complex locking mechanisms needed
  • Thread safety guaranteed at compile time
  • Perfect alignment with Swift 6's strict concurrency model

Comparison with Pointfree Dependencies

Feature TinyDependency Pointfree Dependencies
Complexity Lightweight (~200 LOC) Feature-rich
Sendable Required Optional
Scopes 3 fixed scopes Flexible scope system
Learning Curve Low (like Environment) Medium
Target Scenario Non-view usage All scenarios

TinyDependency focuses on simple scenarios. For advanced features (dependency tracking, automatic mock generation, etc.), consider Pointfree Dependencies.

Examples

See Tests/TinyDependencyTests and Examples.md for complete usage examples.

License

MIT License

Author

Fatbobman

About

A lightweight dependency injection library for Swift with Task-local scoping and test/ preview-aware defaults.

Topics

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages