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/README.md b/README.md index 8750064..e378006 100644 --- a/README.md +++ b/README.md @@ -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(_ 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 + +### @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 7437026..4ee27d2 100644 --- a/Sources/Injection/DependencyInjection.swift +++ b/Sources/Injection/DependencyInjection.swift @@ -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(_ 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(_ 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 { 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 + } + + private mutating func register(_ dependency : T) { dependencyList[ObjectIdentifier(T.self)] = dependency } + + /// 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 1494d71..ee316a8 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(_:)` 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 { + /// 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/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 58% rename from Tests/Injection/DependencyInjectionTests.swift rename to Tests/Injection/NonMainActorResolutionTests.swift index b2cf99c..dcf650c 100644 --- a/Tests/Injection/DependencyInjectionTests.swift +++ b/Tests/Injection/NonMainActorResolutionTests.swift @@ -1,7 +1,17 @@ import Testing @testable import Injection -@Suite(.serialized) struct DependencyInjectionTests { +class NonMainActorResolutionTests { + + init() { + + } + + deinit { + Task { @MainActor in + DependencyInjector.reset() + } + } @Test func testDependencyProviderInline() async throws { // Register dependencies @@ -27,15 +37,26 @@ import Testing #expect(dependency === providedDependency) } + + @Test func expectNilForResolveWithoutRegistration() 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 { } - diff --git a/Tests/Injection/ProtocolResolvementTests.swift b/Tests/Injection/ProtocolResolvementTests.swift new file mode 100644 index 0000000..e0fe06b --- /dev/null +++ b/Tests/Injection/ProtocolResolvementTests.swift @@ -0,0 +1,34 @@ +import Testing +@testable import Injection + +@MainActor +class ProtocolResolvementTests { + + 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 { + +}