Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import PackageDescription
let package = Package(
name: "Injection",
platforms: [
.iOS(.v17),
.watchOS(.v10),
.macOS(.v14),
.visionOS(.v2)
.iOS(.v16),
.watchOS(.v9),
.macOS(.v13),
.visionOS(.v1)
],
products: [
.library(
Expand Down
82 changes: 72 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,92 @@
# Injection
Swift Dependency Injection Framework

A lightweight, thread-safe dependency injection container for Swift applications with property wrapper support.

## Features

- 🔒 Thread-safe registration and resolution with `@MainActor` isolation
- 📦 Simple registration and resolution
- 🏷️ Property wrapper for automatic injection
- 🧪 Testing support with container reset
- ⚡ Lightweight and fast

## Installation

Add this package to your Swift Package Manager dependencies:

```swift
dependencies: [
.package(url: "https://github.com/diamirio/Injection", from: "1.0.0")
]
```

## Usage

```swift
import Injection
```

### Provide Dependency
### Registering Dependencies

Register dependencies during app initialization:

```swift
DependencyInjector.register(MyClass())
// Register concrete types
DependencyInjector.register(MyService())
DependencyInjector.register(UserRepository())

// Register protocol implementations
DependencyInjector.register(NetworkService(), as: NetworkServiceProtocol.self)
```

### Inject Dependency
### Resolving Dependencies

#### Manual Resolution

```swift
let myClass: MyClass = DependencyInjector.resolve()
// Resolve (crashes if not found)
let service: MyService = DependencyInjector.resolve()

// Safe resolve (returns nil if not found)
if let service: MyService = DependencyInjector.safeResolve() {
// Use service safely
}
```

OR
#### Property Wrapper Injection

```swift
class OtherClass {

@Inject
private var myClass: MyClass
class MyViewModel {
@Inject private var service: MyService
@Inject private var repository: UserRepository
}
```
```

### Testing Support

Clear all dependencies between tests:

```swift
func tearDown() {
DependencyInjector.reset()
}
```

## API Reference

### DependencyInjector

- `register<T>(_ dependency: T)` - Register a dependency instance
- `register<T>(_ dependency: T, as type: T.Type)` - Register a dependency instance with explicit type
- `resolve<T>() -> T` - Resolve a dependency (crashes if not found)
- `safeResolve<T>() -> T?` - Safely resolve a dependency (returns nil if not found)
- `reset()` - Clear all registered dependencies

### @Inject Property Wrapper

Automatically injects dependencies using the property wrapper syntax. The dependency must be registered before the property is accessed.

## Thread Safety

All operations are performed on the main thread due to `@MainActor` isolation, ensuring thread safety throughout your application.
142 changes: 133 additions & 9 deletions Sources/Injection/DependencyInjection.swift
Original file line number Diff line number Diff line change
@@ -1,31 +1,155 @@
import Foundation

/// DependencyInjector handles your app dependencies
/// A dependency injection container that manages application dependencies.
///
/// `DependencyInjector` provides a centralized way to register and resolve dependencies
/// throughout your application. It is thread-safe because of `@MainActor` isolation.
///
/// ## Usage
///
/// Register dependencies during app initialization:
/// ```swift
/// DependencyInjector.register(MyService())
/// DependencyInjector.register(MyImplementation(), as: MyProtocol.self)
/// ```
///
/// Resolve dependencies when needed:
/// ```swift
/// let service: MyService = DependencyInjector.resolve()
/// let optionalService: MyService? = DependencyInjector.safeResolve()
/// ```
///
/// ## Thread Safety
/// This struct is marked with `@MainActor` to ensure all operations are performed
/// on the main thread, providing thread safety for dependency registration and resolution.´
@MainActor
public struct DependencyInjector {
private var dependencyList: [ObjectIdentifier : Any] = [:]
static var shared = DependencyInjector()

private init() { }

/// Provide a dependency for injection

/// Registers a dependency instance for later injection.
///
/// This method stores the provided dependency instance in the container,
/// making it available for resolution by type. If a dependency of the same
/// type is already registered, it will be replaced.
///
/// - Parameter dependency: The dependency instance to register.
///
/// ## Example
/// ```swift
/// DependencyInjector.register(MyService())
/// ```
public static func register<T>(_ dependency : T) {
DependencyInjector.shared.register(dependency)
}

/// Resolve a provided dependency
/// Registers a dependency instance for later injection with explicit type specification.
///
/// This method stores the provided dependency instance in the container under
/// the specified type, making it available for resolution by that type. This is
/// useful when you want to register a concrete implementation as a protocol type.
///
/// - Parameters:
/// - dependency: The dependency instance to register.
/// - type: The type to register the dependency as.
///
/// ## Example
/// ```swift
/// DependencyInjector.register(MyImplementation(), as: MyProtocol.self)
/// ```
public static func register<T>(_ dependency: T, as type: T.Type) {
DependencyInjector.shared.register(dependency)
}

/// Resolves a dependency instance by type.
///
/// This method retrieves a previously registered dependency instance of the
/// specified type from the container. If no dependency of the requested type
/// has been registered, this method will trigger a fatal error.
///
/// - Returns: The registered dependency instance of type `T`.
/// - Precondition: A dependency of type `T` must have been previously registered.
///
/// ## Example
/// ```swift
/// let service: MyService = DependencyInjector.resolve()
/// ```
///
/// - Important: This method will crash the app if the dependency is not found.
/// Use `safeResolve()` if you need optional resolution.
public static func resolve<T>() -> T {
return DependencyInjector.shared.resolve()
}

func resolve<T>() -> T {
/// Safely resolves a dependency instance by type, returning nil if not found.
///
/// This method retrieves a previously registered dependency instance of the
/// specified type from the container. Unlike `resolve()`, this method returns
/// `nil` instead of crashing if no dependency of the requested type has been registered.
///
/// - Returns: The registered dependency instance of type `T`, or `nil` if not found.
///
/// ## Example
/// ```swift
/// if let service: MyService = DependencyInjector.safeResolve() {
/// // Use the service
/// } else {
/// // Handle missing dependency gracefully
/// }
/// ```
///
/// - Note: This is the safer alternative to `resolve()` when you're unsure
/// if a dependency has been registered.
public static func safeResolve<T>() -> T? {
return DependencyInjector.shared.safeResolve()
}

/// Resets the dependency injection container, clearing all registered dependencies.
///
/// This method creates a new instance of the dependency injector, effectively
/// removing all previously registered dependencies. This is particularly useful
/// for testing scenarios where you need a clean slate between test cases.
///
/// ## Example
/// ```swift
/// // Register some dependencies
/// DependencyInjector.register(MyService())
/// DependencyInjector.register(MyRepository())
///
/// // Clear all dependencies
/// DependencyInjector.reset()
///
/// // Container is now empty - resolving will fail until dependencies are re-registered
/// ```
///
/// - Warning: After calling this method, all previously registered dependencies
/// will be lost. Any subsequent calls to `resolve()` or `safeResolve()`
/// will fail until dependencies are re-registered.
///
/// - Note: This method is commonly used in unit tests to ensure test isolation
/// and prevent dependencies from one test affecting another.
public static func reset() {
shared = DependencyInjector()
}

private func resolve<T>() -> T {
guard let t = dependencyList[ObjectIdentifier(T.self)] as? T else {
fatalError("No provider registered for type \(T.self)")
}
return t
}

mutating func register<T>(_ dependency : T) {
private func safeResolve<T>() -> T? {
guard let t = dependencyList[ObjectIdentifier(T.self)] as? T else {
return nil
}
return t
}

private mutating func register<T>(_ dependency : T) {
dependencyList[ObjectIdentifier(T.self)] = dependency
}

/// Singleton instance of the DependencyInjector.
internal static var shared = DependencyInjector()
private init() { }
}
38 changes: 36 additions & 2 deletions Sources/Injection/Inject.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
import Foundation

/// Use to inject a already provided dependency
/// A property wrapper that automatically injects dependencies.
///
/// `Inject` provides a convenient way to automatically resolve and inject dependencies
/// into your types using Swift's property wrapper syntax. The dependency must be
/// previously registered with `DependencyInjector` before use.
///
/// ## Usage
///
/// Use the `@Inject` property wrapper to automatically inject dependencies:
/// ```swift
/// class MyViewController {
/// @Inject private var service: MyService
/// @Inject private var repository: MyRepository
/// }
/// ```
///
/// ## Requirements
/// - The dependency type `T` must be previously registered with `DependencyInjector.register(_:)` or `DependencyInjector.register(_:as:)`
/// - This property wrapper uses `DependencyInjector.resolve()` internally, so it will
/// crash if the dependency is not found
///
/// ## Thread Safety
/// This property wrapper is marked with `@MainActor` to ensure dependency resolution
/// happens on the main thread, maintaining thread safety.
///
/// - Important: Make sure to register your dependencies before creating instances
/// that use this property wrapper, typically during app initialization.
@MainActor
@propertyWrapper public struct Inject<T> {
/// The injected dependency instance.
public var wrappedValue: T

/// Initializes the property wrapper and resolves the dependency.
///
/// This initializer automatically resolves the dependency of type `T`
/// from the `DependencyInjector`. The dependency must have been previously
/// registered or this will result in a fatal error.
///
/// - Precondition: A dependency of type `T` must be registered with `DependencyInjector`.
public init() {
self.wrappedValue = DependencyInjector.shared.resolve()
self.wrappedValue = DependencyInjector.resolve()
}
}
63 changes: 63 additions & 0 deletions Tests/Injection/MainActorResolutionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Testing
@testable import Injection

@MainActor
class MainActorResolutionTests {

init() {

}

deinit {
Task { @MainActor in
DependencyInjector.reset()
}
}

@Test func testDependencyProviderInline() async throws {
// Register dependencies
let providedDependency = MyTestDependency()
DependencyInjector.register(providedDependency)
DependencyInjector.register(MySecondDependency())

// Resolve dependencies
let dependency: MyTestDependency = DependencyInjector.resolve()

#expect(dependency === providedDependency)
}

@Test func testDependencyProviderPropertyWrapper() async throws {
// Register dependencies
let providedDependency = MyTestDependency()
DependencyInjector.register(providedDependency)
DependencyInjector.register(MySecondDependency())

// Resolve dependencies
@Inject
var dependency: MyTestDependency

#expect(dependency === providedDependency)
}

@Test func expectNilForResolveWithoutRegistration() async throws {
let dependency: MyTestDependency? = DependencyInjector.safeResolve()
#expect(dependency == nil)

let providedDependency = MyTestDependency()
DependencyInjector.register(providedDependency)

let resolvedDependency: MyTestDependency? = DependencyInjector.safeResolve()

#expect(resolvedDependency === providedDependency)
}
}

/// Dependency just for testing purposes
fileprivate final class MyTestDependency {

}

/// Dependency just for testing purposes
fileprivate final class MySecondDependency {

}
Loading
Loading