From ffa6065a35677b316d9d1a00444d25b0a0ab241a Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 5 Aug 2025 12:40:03 +0200 Subject: [PATCH 1/5] Decreased min version requirement, added safeResolve function, added documentation --- Package.swift | 8 +- Sources/Injection/DependencyInjection.swift | 124 ++++++++++++++++-- Sources/Injection/Inject.swift | 38 +++++- .../Injection/DependencyInjectionTests.swift | 29 +++- 4 files changed, 182 insertions(+), 17 deletions(-) diff --git a/Package.swift b/Package.swift index 7a72ffc..dadd2b5 100644 --- a/Package.swift +++ b/Package.swift @@ -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( diff --git a/Sources/Injection/DependencyInjection.swift b/Sources/Injection/DependencyInjection.swift index 7437026..00c42c5 100644 --- a/Sources/Injection/DependencyInjection.swift +++ b/Sources/Injection/DependencyInjection.swift @@ -1,31 +1,139 @@ 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()) +/// ``` +/// +/// 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()) + /// DependencyInjector.register(MyImplementation()) + /// ``` public static func register(_ dependency : T) { DependencyInjector.shared.register(dependency) } - /// Resolve a provided 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 { return DependencyInjector.shared.resolve() } - func resolve() -> 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? { + 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 { guard let t = dependencyList[ObjectIdentifier(T.self)] as? T else { fatalError("No provider registered for type \(T.self)") } return t } - mutating func register(_ dependency : T) { + private func safeResolve() -> T? { + guard let t = dependencyList[ObjectIdentifier(T.self)] as? T else { + return nil + } + return t + } + + mutating private func register(_ dependency : T) { dependencyList[ObjectIdentifier(T.self)] = dependency } + + /// Singelton instance of the DependencyInjector. + internal static var shared = DependencyInjector() + private init() { } } diff --git a/Sources/Injection/Inject.swift b/Sources/Injection/Inject.swift index 1494d71..711c008 100644 --- a/Sources/Injection/Inject.swift +++ b/Sources/Injection/Inject.swift @@ -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(_:)` +/// - 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 { + /// 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() } } diff --git a/Tests/Injection/DependencyInjectionTests.swift b/Tests/Injection/DependencyInjectionTests.swift index b2cf99c..a2f5dad 100644 --- a/Tests/Injection/DependencyInjectionTests.swift +++ b/Tests/Injection/DependencyInjectionTests.swift @@ -1,7 +1,18 @@ import Testing @testable import Injection -@Suite(.serialized) struct DependencyInjectionTests { +class DependencyInjectionTests { + + init() { + + } + + deinit { + print("CLEAR") + Task { @MainActor in + DependencyInjector.reset() + } + } @Test func testDependencyProviderInline() async throws { // Register dependencies @@ -27,15 +38,27 @@ import Testing #expect(dependency === providedDependency) } + + @Test func expectFailToResolveWithoutRegistration() async throws { + let dependency: MyTestDependency? = await DependencyInjector.safeResolve() + #expect(dependency == nil) + + let providedDependency = MyTestDependency() + await DependencyInjector.register(providedDependency) + + let resolvedDependency: MyTestDependency? = await DependencyInjector.safeResolve() + + #expect(resolvedDependency === providedDependency) + } } /// Dependency just for testing purposes -final class MyTestDependency: Sendable { +fileprivate final class MyTestDependency: Sendable { } /// Dependency just for testing purposes -final class MySecondDependency: Sendable { +fileprivate final class MySecondDependency: Sendable { } From cb2dc1ee8b009fd5d7681d1ab29a35fcac1c4c7a Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 5 Aug 2025 13:03:56 +0200 Subject: [PATCH 2/5] Added possibility to resolve based on protocol --- README.md | 84 +++++++++++++++++-- Sources/Injection/DependencyInjection.swift | 24 +++++- .../Injection/MainActorResolutionTests.swift | 63 ++++++++++++++ ...wift => NonMainActorResolutionTests.swift} | 8 +- Tests/Injection/ProtocolResolvmentTests.swift | 34 ++++++++ 5 files changed, 195 insertions(+), 18 deletions(-) create mode 100644 Tests/Injection/MainActorResolutionTests.swift rename Tests/Injection/{DependencyInjectionTests.swift => NonMainActorResolutionTests.swift} (90%) create mode 100644 Tests/Injection/ProtocolResolvmentTests.swift diff --git a/README.md b/README.md index 8750064..83ec3ca 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,96 @@ # 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 { +class MyViewController { + @Inject private var service: MyService + @Inject private var repository: UserRepository - @Inject - private var myClass: MyClass + func viewDidLoad() { + // Dependencies are automatically injected + service.performAction() + } } -``` +``` + +### Testing Support + +Clear all dependencies between tests: + +```swift +func tearDown() { + DependencyInjector.reset() +} +``` + +## API Reference + +### DependencyInjector + +- `register(_ dependency: T)` - Register a dependency instance +- `resolve() -> T` - Resolve a dependency (crashes if not found) +- `safeResolve() -> 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. diff --git a/Sources/Injection/DependencyInjection.swift b/Sources/Injection/DependencyInjection.swift index 00c42c5..c348932 100644 --- a/Sources/Injection/DependencyInjection.swift +++ b/Sources/Injection/DependencyInjection.swift @@ -10,7 +10,7 @@ import Foundation /// Register dependencies during app initialization: /// ```swift /// DependencyInjector.register(MyService()) -/// DependencyInjector.register(MyImplementation()) +/// DependencyInjector.register(MyImplementation(), as: MyProtocol.self) /// ``` /// /// Resolve dependencies when needed: @@ -26,7 +26,6 @@ import Foundation public struct DependencyInjector { private var dependencyList: [ObjectIdentifier : Any] = [:] - /// Registers a dependency instance for later injection. /// /// This method stores the provided dependency instance in the container, @@ -38,12 +37,29 @@ public struct DependencyInjector { /// ## Example /// ```swift /// DependencyInjector.register(MyService()) - /// DependencyInjector.register(MyImplementation()) /// ``` public static func register(_ dependency : T) { DependencyInjector.shared.register(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(_ 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 @@ -129,7 +145,7 @@ public struct DependencyInjector { return t } - mutating private func register(_ dependency : T) { + private mutating func register(_ dependency : T) { dependencyList[ObjectIdentifier(T.self)] = dependency } diff --git a/Tests/Injection/MainActorResolutionTests.swift b/Tests/Injection/MainActorResolutionTests.swift new file mode 100644 index 0000000..6260370 --- /dev/null +++ b/Tests/Injection/MainActorResolutionTests.swift @@ -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 { + +} diff --git a/Tests/Injection/DependencyInjectionTests.swift b/Tests/Injection/NonMainActorResolutionTests.swift similarity index 90% rename from Tests/Injection/DependencyInjectionTests.swift rename to Tests/Injection/NonMainActorResolutionTests.swift index a2f5dad..dcf650c 100644 --- a/Tests/Injection/DependencyInjectionTests.swift +++ b/Tests/Injection/NonMainActorResolutionTests.swift @@ -1,14 +1,13 @@ import Testing @testable import Injection -class DependencyInjectionTests { +class NonMainActorResolutionTests { init() { } deinit { - print("CLEAR") Task { @MainActor in DependencyInjector.reset() } @@ -39,7 +38,7 @@ class DependencyInjectionTests { #expect(dependency === providedDependency) } - @Test func expectFailToResolveWithoutRegistration() async throws { + @Test func expectNilForResolveWithoutRegistration() async throws { let dependency: MyTestDependency? = await DependencyInjector.safeResolve() #expect(dependency == nil) @@ -53,7 +52,7 @@ class DependencyInjectionTests { } /// Dependency just for testing purposes -fileprivate final class MyTestDependency: Sendable { +fileprivate final class MyTestDependency: Sendable{ } @@ -61,4 +60,3 @@ fileprivate final class MyTestDependency: Sendable { fileprivate final class MySecondDependency: Sendable { } - diff --git a/Tests/Injection/ProtocolResolvmentTests.swift b/Tests/Injection/ProtocolResolvmentTests.swift new file mode 100644 index 0000000..279c574 --- /dev/null +++ b/Tests/Injection/ProtocolResolvmentTests.swift @@ -0,0 +1,34 @@ +import Testing +@testable import Injection + +@MainActor +class ProtocolResolvmentTests { + + init() { + + } + + deinit { + Task { @MainActor in + DependencyInjector.reset() + } + } + + @Test func testProtocolResolve() async throws { + let providedDependency = MyProtocolImplementation() + DependencyInjector.register(providedDependency, as: MyProtocol.self) + + let resolvedDependency: MyProtocol = DependencyInjector.resolve() + + #expect(resolvedDependency === providedDependency) + } +} + + +fileprivate protocol MyProtocol: AnyObject { + +} + +fileprivate final class MyProtocolImplementation: MyProtocol { + +} From 55b9acd79b306808f459d2b8a49b2329ca8134b0 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 5 Aug 2025 13:38:56 +0200 Subject: [PATCH 3/5] Brought documentation up-to-date --- README.md | 1 + Sources/Injection/DependencyInjection.swift | 2 +- Sources/Injection/Inject.swift | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 83ec3ca..e691b4d 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ func tearDown() { ### DependencyInjector - `register(_ dependency: T)` - Register a dependency instance +- `register(_ dependency: T, as type: T.Type)` - Register a dependency instance with explicit type - `resolve() -> T` - Resolve a dependency (crashes if not found) - `safeResolve() -> T?` - Safely resolve a dependency (returns nil if not found) - `reset()` - Clear all registered dependencies diff --git a/Sources/Injection/DependencyInjection.swift b/Sources/Injection/DependencyInjection.swift index c348932..4ee27d2 100644 --- a/Sources/Injection/DependencyInjection.swift +++ b/Sources/Injection/DependencyInjection.swift @@ -149,7 +149,7 @@ public struct DependencyInjector { dependencyList[ObjectIdentifier(T.self)] = dependency } - /// Singelton instance of the DependencyInjector. + /// Singleton instance of the DependencyInjector. internal static var shared = DependencyInjector() private init() { } } diff --git a/Sources/Injection/Inject.swift b/Sources/Injection/Inject.swift index 711c008..ee316a8 100644 --- a/Sources/Injection/Inject.swift +++ b/Sources/Injection/Inject.swift @@ -17,7 +17,7 @@ import Foundation /// ``` /// /// ## Requirements -/// - The dependency type `T` must be previously registered with `DependencyInjector.register(_:)` +/// - 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 /// From 6a83aa1401c64c337edf25e74fa9b052cb522264 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Wed, 6 Aug 2025 08:42:24 +0200 Subject: [PATCH 4/5] ViewModel Example --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index e691b4d..e378006 100644 --- a/README.md +++ b/README.md @@ -57,14 +57,9 @@ if let service: MyService = DependencyInjector.safeResolve() { #### Property Wrapper Injection ```swift -class MyViewController { +class MyViewModel { @Inject private var service: MyService @Inject private var repository: UserRepository - - func viewDidLoad() { - // Dependencies are automatically injected - service.performAction() - } } ``` From 1647ecd60f450ae60ca99131be0c34333e8a5dda Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Wed, 6 Aug 2025 08:42:57 +0200 Subject: [PATCH 5/5] Renamed ProtocolResolvementTests --- ...ocolResolvmentTests.swift => ProtocolResolvementTests.swift} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Tests/Injection/{ProtocolResolvmentTests.swift => ProtocolResolvementTests.swift} (95%) diff --git a/Tests/Injection/ProtocolResolvmentTests.swift b/Tests/Injection/ProtocolResolvementTests.swift similarity index 95% rename from Tests/Injection/ProtocolResolvmentTests.swift rename to Tests/Injection/ProtocolResolvementTests.swift index 279c574..e0fe06b 100644 --- a/Tests/Injection/ProtocolResolvmentTests.swift +++ b/Tests/Injection/ProtocolResolvementTests.swift @@ -2,7 +2,7 @@ import Testing @testable import Injection @MainActor -class ProtocolResolvmentTests { +class ProtocolResolvementTests { init() {