Skip to content

Matejkob/swift-di

Repository files navigation

Swift DI

A modern, lightweight dependency injection framework for Swift, leveraging Swift macros for compile-time safety and performance.

Swift Platforms

Features

  • 🏗️ Macro-driven: Use @Register for clean, compile-time dependency registration
  • 📱 Multi-platform: Supports iOS, macOS, tvOS, watchOS, and Mac Catalyst
  • 🔒 Sendable: Full Swift concurrency support with @TaskLocal scoping
  • Performance: Zero-runtime overhead with compile-time code generation
  • 🎯 Type-safe: Full type safety with Swift's type system
  • 🧹 Clean API: Minimal boilerplate with intuitive property wrapper syntax

Quick Start

Installation

Add Swift DI to your project using Swift Package Manager:

dependencies: [
    .package(url: "https://github.com/Matejkob/swift-di.git", from: "1.0.0")
]

Basic Usage

  1. Define your services:
protocol NetworkService {
    func fetchData() async throws -> Data
}

struct APIService: NetworkService {
    func fetchData() async throws -> Data {
        // Implementation here
    }
}
  1. Register dependencies:
import DI
import DIMacros

extension DependencyContainer {
    @Register var networkService: NetworkService = APIService()
    @Register var apiKey: String = "default-api-key"
}
  1. Inject dependencies:
struct UserRepository {
    @Dependency(\.networkService) var networkService
    @Dependency(\.apiKey) var apiKey
    
    func fetchUser(id: String) async throws -> User {
        // Use injected dependencies
        let data = try await networkService.fetchData()
        // Process data...
    }
}

Advanced Usage

Protocol-based Dependencies

protocol UserRepository {
    func fetchUser(id: String) async throws -> User
}

struct RemoteUserRepository: UserRepository {
    @Dependency(\.networkService) var networkService
    
    func fetchUser(id: String) async throws -> User {
        // Remote implementation
    }
}

struct LocalUserRepository: UserRepository {
    func fetchUser(id: String) async throws -> User {
        // Local implementation
    }
}

extension DependencyContainer {
    @Register var userRepository: UserRepository = RemoteUserRepository()
}

Configuration Dependencies

struct AppConfiguration {
    let baseURL: URL
    let timeout: TimeInterval
    let retryCount: Int
}

extension DependencyContainer {
    @Register var config: AppConfiguration = AppConfiguration(
        baseURL: URL(string: "https://api.example.com")!,
        timeout: 30.0,
        retryCount: 3
    )
}

Testing with Dependency Injection

import XCTest
@testable import YourApp

class UserRepositoryTests: XCTestCase {
    func testFetchUser() async throws {
        // Override dependencies for testing
        await DependencyContainer.$current.withValue(DependencyContainer()) {
            DependencyContainer.current[\.networkService] = MockNetworkService()
            
            let repository = UserRepository()
            let user = try await repository.fetchUser(id: "123")
            
            XCTAssertEqual(user.id, "123")
        }
    }
}

struct MockNetworkService: NetworkService {
    func fetchData() async throws -> Data {
        // Mock implementation
        return Data()
    }
}

SwiftUI Integration

import SwiftUI

struct ContentView: View {
    @Dependency(\.userRepository) var userRepository
    @State private var user: User?
    
    var body: some View {
        VStack {
            if let user = user {
                Text("Hello, \(user.name)!")
            } else {
                Text("Loading...")
            }
        }
        .task {
            do {
                user = try await userRepository.fetchUser(id: "current")
            } catch {
                print("Failed to fetch user: \(error)")
            }
        }
    }
}

Architecture

Swift DI consists of three main components:

1. DependencyKey Protocol

Defines the contract for dependency keys:

protocol DependencyKey {
    associatedtype Value: Sendable = Self
    static var defaultValue: Self.Value { get }
}

2. DependencyContainer

Thread-safe container using @TaskLocal for scoped access:

@TaskLocal static var current: DependencyContainer

3. @Register Macro

Compile-time code generation for dependency registration:

// This code:
@Register var service: MyService = MyService()

// Generates:
private struct __key__service: DependencyKey {
    static let defaultValue: MyService = MyService()
}

var service: MyService {
    get { self[__key__service.self] }
    set { self[__key__service.self] = newValue }
}

Requirements

  • Swift 6.1+
  • iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

Swift DI is available under the MIT license. See the LICENSE file for more info.

About

No description, website, or topics provided.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages