Skip to content

Commit 57a66d3

Browse files
authored
Merge pull request #1 from diamirio/feature/safe-resolve
Safe Resolve & Documentation
2 parents cb44371 + 1647ecd commit 57a66d3

File tree

7 files changed

+367
-29
lines changed

7 files changed

+367
-29
lines changed

Package.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import PackageDescription
55
let package = Package(
66
name: "Injection",
77
platforms: [
8-
.iOS(.v17),
9-
.watchOS(.v10),
10-
.macOS(.v14),
11-
.visionOS(.v2)
8+
.iOS(.v16),
9+
.watchOS(.v9),
10+
.macOS(.v13),
11+
.visionOS(.v1)
1212
],
1313
products: [
1414
.library(

README.md

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,92 @@
11
# Injection
22
Swift Dependency Injection Framework
33

4+
A lightweight, thread-safe dependency injection container for Swift applications with property wrapper support.
5+
6+
## Features
7+
8+
- 🔒 Thread-safe registration and resolution with `@MainActor` isolation
9+
- 📦 Simple registration and resolution
10+
- 🏷️ Property wrapper for automatic injection
11+
- 🧪 Testing support with container reset
12+
- ⚡ Lightweight and fast
13+
14+
## Installation
15+
16+
Add this package to your Swift Package Manager dependencies:
17+
18+
```swift
19+
dependencies: [
20+
.package(url: "https://github.com/diamirio/Injection", from: "1.0.0")
21+
]
22+
```
23+
424
## Usage
525

626
```swift
727
import Injection
828
```
929

10-
### Provide Dependency
30+
### Registering Dependencies
31+
32+
Register dependencies during app initialization:
1133

1234
```swift
13-
DependencyInjector.register(MyClass())
35+
// Register concrete types
36+
DependencyInjector.register(MyService())
37+
DependencyInjector.register(UserRepository())
38+
39+
// Register protocol implementations
40+
DependencyInjector.register(NetworkService(), as: NetworkServiceProtocol.self)
1441
```
1542

16-
### Inject Dependency
43+
### Resolving Dependencies
44+
45+
#### Manual Resolution
1746

1847
```swift
19-
let myClass: MyClass = DependencyInjector.resolve()
48+
// Resolve (crashes if not found)
49+
let service: MyService = DependencyInjector.resolve()
50+
51+
// Safe resolve (returns nil if not found)
52+
if let service: MyService = DependencyInjector.safeResolve() {
53+
// Use service safely
54+
}
2055
```
2156

22-
OR
57+
#### Property Wrapper Injection
2358

2459
```swift
25-
class OtherClass {
26-
27-
@Inject
28-
private var myClass: MyClass
60+
class MyViewModel {
61+
@Inject private var service: MyService
62+
@Inject private var repository: UserRepository
2963
}
30-
```
64+
```
65+
66+
### Testing Support
67+
68+
Clear all dependencies between tests:
69+
70+
```swift
71+
func tearDown() {
72+
DependencyInjector.reset()
73+
}
74+
```
75+
76+
## API Reference
77+
78+
### DependencyInjector
79+
80+
- `register<T>(_ dependency: T)` - Register a dependency instance
81+
- `register<T>(_ dependency: T, as type: T.Type)` - Register a dependency instance with explicit type
82+
- `resolve<T>() -> T` - Resolve a dependency (crashes if not found)
83+
- `safeResolve<T>() -> T?` - Safely resolve a dependency (returns nil if not found)
84+
- `reset()` - Clear all registered dependencies
85+
86+
### @Inject Property Wrapper
87+
88+
Automatically injects dependencies using the property wrapper syntax. The dependency must be registered before the property is accessed.
89+
90+
## Thread Safety
91+
92+
All operations are performed on the main thread due to `@MainActor` isolation, ensuring thread safety throughout your application.
Lines changed: 133 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,155 @@
11
import Foundation
22

3-
/// DependencyInjector handles your app dependencies
3+
/// A dependency injection container that manages application dependencies.
4+
///
5+
/// `DependencyInjector` provides a centralized way to register and resolve dependencies
6+
/// throughout your application. It is thread-safe because of `@MainActor` isolation.
7+
///
8+
/// ## Usage
9+
///
10+
/// Register dependencies during app initialization:
11+
/// ```swift
12+
/// DependencyInjector.register(MyService())
13+
/// DependencyInjector.register(MyImplementation(), as: MyProtocol.self)
14+
/// ```
15+
///
16+
/// Resolve dependencies when needed:
17+
/// ```swift
18+
/// let service: MyService = DependencyInjector.resolve()
19+
/// let optionalService: MyService? = DependencyInjector.safeResolve()
20+
/// ```
21+
///
22+
/// ## Thread Safety
23+
/// This struct is marked with `@MainActor` to ensure all operations are performed
24+
/// on the main thread, providing thread safety for dependency registration and resolution.´
425
@MainActor
526
public struct DependencyInjector {
627
private var dependencyList: [ObjectIdentifier : Any] = [:]
7-
static var shared = DependencyInjector()
8-
9-
private init() { }
10-
11-
/// Provide a dependency for injection
28+
29+
/// Registers a dependency instance for later injection.
30+
///
31+
/// This method stores the provided dependency instance in the container,
32+
/// making it available for resolution by type. If a dependency of the same
33+
/// type is already registered, it will be replaced.
34+
///
35+
/// - Parameter dependency: The dependency instance to register.
36+
///
37+
/// ## Example
38+
/// ```swift
39+
/// DependencyInjector.register(MyService())
40+
/// ```
1241
public static func register<T>(_ dependency : T) {
1342
DependencyInjector.shared.register(dependency)
1443
}
1544

16-
/// Resolve a provided dependency
45+
/// Registers a dependency instance for later injection with explicit type specification.
46+
///
47+
/// This method stores the provided dependency instance in the container under
48+
/// the specified type, making it available for resolution by that type. This is
49+
/// useful when you want to register a concrete implementation as a protocol type.
50+
///
51+
/// - Parameters:
52+
/// - dependency: The dependency instance to register.
53+
/// - type: The type to register the dependency as.
54+
///
55+
/// ## Example
56+
/// ```swift
57+
/// DependencyInjector.register(MyImplementation(), as: MyProtocol.self)
58+
/// ```
59+
public static func register<T>(_ dependency: T, as type: T.Type) {
60+
DependencyInjector.shared.register(dependency)
61+
}
62+
63+
/// Resolves a dependency instance by type.
64+
///
65+
/// This method retrieves a previously registered dependency instance of the
66+
/// specified type from the container. If no dependency of the requested type
67+
/// has been registered, this method will trigger a fatal error.
68+
///
69+
/// - Returns: The registered dependency instance of type `T`.
70+
/// - Precondition: A dependency of type `T` must have been previously registered.
71+
///
72+
/// ## Example
73+
/// ```swift
74+
/// let service: MyService = DependencyInjector.resolve()
75+
/// ```
76+
///
77+
/// - Important: This method will crash the app if the dependency is not found.
78+
/// Use `safeResolve()` if you need optional resolution.
1779
public static func resolve<T>() -> T {
1880
return DependencyInjector.shared.resolve()
1981
}
2082

21-
func resolve<T>() -> T {
83+
/// Safely resolves a dependency instance by type, returning nil if not found.
84+
///
85+
/// This method retrieves a previously registered dependency instance of the
86+
/// specified type from the container. Unlike `resolve()`, this method returns
87+
/// `nil` instead of crashing if no dependency of the requested type has been registered.
88+
///
89+
/// - Returns: The registered dependency instance of type `T`, or `nil` if not found.
90+
///
91+
/// ## Example
92+
/// ```swift
93+
/// if let service: MyService = DependencyInjector.safeResolve() {
94+
/// // Use the service
95+
/// } else {
96+
/// // Handle missing dependency gracefully
97+
/// }
98+
/// ```
99+
///
100+
/// - Note: This is the safer alternative to `resolve()` when you're unsure
101+
/// if a dependency has been registered.
102+
public static func safeResolve<T>() -> T? {
103+
return DependencyInjector.shared.safeResolve()
104+
}
105+
106+
/// Resets the dependency injection container, clearing all registered dependencies.
107+
///
108+
/// This method creates a new instance of the dependency injector, effectively
109+
/// removing all previously registered dependencies. This is particularly useful
110+
/// for testing scenarios where you need a clean slate between test cases.
111+
///
112+
/// ## Example
113+
/// ```swift
114+
/// // Register some dependencies
115+
/// DependencyInjector.register(MyService())
116+
/// DependencyInjector.register(MyRepository())
117+
///
118+
/// // Clear all dependencies
119+
/// DependencyInjector.reset()
120+
///
121+
/// // Container is now empty - resolving will fail until dependencies are re-registered
122+
/// ```
123+
///
124+
/// - Warning: After calling this method, all previously registered dependencies
125+
/// will be lost. Any subsequent calls to `resolve()` or `safeResolve()`
126+
/// will fail until dependencies are re-registered.
127+
///
128+
/// - Note: This method is commonly used in unit tests to ensure test isolation
129+
/// and prevent dependencies from one test affecting another.
130+
public static func reset() {
131+
shared = DependencyInjector()
132+
}
133+
134+
private func resolve<T>() -> T {
22135
guard let t = dependencyList[ObjectIdentifier(T.self)] as? T else {
23136
fatalError("No provider registered for type \(T.self)")
24137
}
25138
return t
26139
}
27140

28-
mutating func register<T>(_ dependency : T) {
141+
private func safeResolve<T>() -> T? {
142+
guard let t = dependencyList[ObjectIdentifier(T.self)] as? T else {
143+
return nil
144+
}
145+
return t
146+
}
147+
148+
private mutating func register<T>(_ dependency : T) {
29149
dependencyList[ObjectIdentifier(T.self)] = dependency
30150
}
151+
152+
/// Singleton instance of the DependencyInjector.
153+
internal static var shared = DependencyInjector()
154+
private init() { }
31155
}

Sources/Injection/Inject.swift

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,45 @@
11
import Foundation
22

3-
/// Use to inject a already provided dependency
3+
/// A property wrapper that automatically injects dependencies.
4+
///
5+
/// `Inject` provides a convenient way to automatically resolve and inject dependencies
6+
/// into your types using Swift's property wrapper syntax. The dependency must be
7+
/// previously registered with `DependencyInjector` before use.
8+
///
9+
/// ## Usage
10+
///
11+
/// Use the `@Inject` property wrapper to automatically inject dependencies:
12+
/// ```swift
13+
/// class MyViewController {
14+
/// @Inject private var service: MyService
15+
/// @Inject private var repository: MyRepository
16+
/// }
17+
/// ```
18+
///
19+
/// ## Requirements
20+
/// - The dependency type `T` must be previously registered with `DependencyInjector.register(_:)` or `DependencyInjector.register(_:as:)`
21+
/// - This property wrapper uses `DependencyInjector.resolve()` internally, so it will
22+
/// crash if the dependency is not found
23+
///
24+
/// ## Thread Safety
25+
/// This property wrapper is marked with `@MainActor` to ensure dependency resolution
26+
/// happens on the main thread, maintaining thread safety.
27+
///
28+
/// - Important: Make sure to register your dependencies before creating instances
29+
/// that use this property wrapper, typically during app initialization.
430
@MainActor
531
@propertyWrapper public struct Inject<T> {
32+
/// The injected dependency instance.
633
public var wrappedValue: T
734

35+
/// Initializes the property wrapper and resolves the dependency.
36+
///
37+
/// This initializer automatically resolves the dependency of type `T`
38+
/// from the `DependencyInjector`. The dependency must have been previously
39+
/// registered or this will result in a fatal error.
40+
///
41+
/// - Precondition: A dependency of type `T` must be registered with `DependencyInjector`.
842
public init() {
9-
self.wrappedValue = DependencyInjector.shared.resolve()
43+
self.wrappedValue = DependencyInjector.resolve()
1044
}
1145
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import Testing
2+
@testable import Injection
3+
4+
@MainActor
5+
class MainActorResolutionTests {
6+
7+
init() {
8+
9+
}
10+
11+
deinit {
12+
Task { @MainActor in
13+
DependencyInjector.reset()
14+
}
15+
}
16+
17+
@Test func testDependencyProviderInline() async throws {
18+
// Register dependencies
19+
let providedDependency = MyTestDependency()
20+
DependencyInjector.register(providedDependency)
21+
DependencyInjector.register(MySecondDependency())
22+
23+
// Resolve dependencies
24+
let dependency: MyTestDependency = DependencyInjector.resolve()
25+
26+
#expect(dependency === providedDependency)
27+
}
28+
29+
@Test func testDependencyProviderPropertyWrapper() async throws {
30+
// Register dependencies
31+
let providedDependency = MyTestDependency()
32+
DependencyInjector.register(providedDependency)
33+
DependencyInjector.register(MySecondDependency())
34+
35+
// Resolve dependencies
36+
@Inject
37+
var dependency: MyTestDependency
38+
39+
#expect(dependency === providedDependency)
40+
}
41+
42+
@Test func expectNilForResolveWithoutRegistration() async throws {
43+
let dependency: MyTestDependency? = DependencyInjector.safeResolve()
44+
#expect(dependency == nil)
45+
46+
let providedDependency = MyTestDependency()
47+
DependencyInjector.register(providedDependency)
48+
49+
let resolvedDependency: MyTestDependency? = DependencyInjector.safeResolve()
50+
51+
#expect(resolvedDependency === providedDependency)
52+
}
53+
}
54+
55+
/// Dependency just for testing purposes
56+
fileprivate final class MyTestDependency {
57+
58+
}
59+
60+
/// Dependency just for testing purposes
61+
fileprivate final class MySecondDependency {
62+
63+
}

0 commit comments

Comments
 (0)