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
5 changes: 3 additions & 2 deletions .github/workflows/swift-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ jobs:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Select Xcode 16.4
run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26.1.1'
- name: Build
run: swift build -v
- name: Run tests
Expand Down
2 changes: 1 addition & 1 deletion .swiftpm/Injection.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"testTargets" : [
{
"parallelizable" : false,
"parallelizable" : true,
"target" : {
"containerPath" : "container:",
"identifier" : "InjectionTests",
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,30 @@ class MyViewModel {
}
```

### Managing Dependencies

Remove specific dependencies without clearing the entire container:

```swift
// Register dependencies
DependencyInjector.register(MyService())
DependencyInjector.register(UserRepository())

// Option 1: Remove by type
DependencyInjector.remove(MyService.self)

// Option 2: Remove by instance (when you have a reference)
let service = MyService()
DependencyInjector.register(service)
DependencyInjector.remove(service)

// UserRepository is still available
let repo: UserRepository = DependencyInjector.resolve() // Works
let removed: MyService = DependencyInjector.resolve() // Fatal error - removed
```

This is useful when you need to swap out specific dependencies during testing or when cleaning up individual dependencies that are no longer needed.

### Testing Support

Clear all dependencies between tests:
Expand All @@ -73,6 +97,18 @@ func tearDown() {
}
```

Or remove specific dependencies for testing:

```swift
func testWithMockService() {
// Remove the real service
DependencyInjector.remove(MyService.self)

// Register a mock
DependencyInjector.register(MockService())
}
```

## API Reference

### DependencyInjector
Expand All @@ -81,6 +117,8 @@ func tearDown() {
- `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)
- `remove<T>(_ type: T.Type)` - Remove a specific registered dependency by type
- `remove<T>(_ dependency: T)` - Remove a specific registered dependency by instance
- `reset()` - Clear all registered dependencies

### @Inject Property Wrapper
Expand Down
66 changes: 64 additions & 2 deletions Sources/Injection/DependencyInjection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,61 @@ public struct DependencyInjector {
public static func reset() {
shared = DependencyInjector()
}


/// Removes a registered dependency of the specified type from the container.
///
/// This method removes a previously registered dependency instance of the
/// specified type from the container. If no dependency of the requested type
/// has been registered, this method silently succeeds without any effect.
///
/// - Parameter type: The type of dependency to remove.
///
/// ## Example
/// ```swift
/// // Register a dependency
/// DependencyInjector.register(MyService())
///
/// // Remove it
/// DependencyInjector.remove(MyService.self)
///
/// // Subsequent resolve will fail
/// let service: MyService = DependencyInjector.resolve() // Fatal error
/// ```
///
/// - Note: This is useful for testing scenarios where you need to swap out
/// specific dependencies without clearing the entire container with `reset()`.
public static func remove<T>(_ type: T.Type) {
DependencyInjector.shared.remove(type)
}

/// Removes a registered dependency by its instance from the container.
///
/// This method removes a previously registered dependency instance from the
/// container by extracting the type from the provided instance. If no dependency
/// of the inferred type has been registered, this method silently succeeds
/// without any effect.
///
/// - Parameter dependency: The dependency instance to remove (type is inferred).
///
/// ## Example
/// ```swift
/// // Register a dependency
/// let service = MyService()
/// DependencyInjector.register(service)
///
/// // Remove it by passing the instance
/// DependencyInjector.remove(service)
///
/// // Subsequent resolve will fail
/// let resolved: MyService = DependencyInjector.resolve() // Fatal error
/// ```
///
/// - Note: This overload provides convenience when you have a reference to the
/// dependency instance and want to remove it without explicitly specifying the type.
public static func remove<T>(_ dependency: T) {
DependencyInjector.shared.remove(dependency)
}

private func resolve<T>() -> T {
guard let t = dependencyList[ObjectIdentifier(T.self)] as? T else {
fatalError("No provider registered for type \(T.self)")
Expand All @@ -148,7 +202,15 @@ public struct DependencyInjector {
private mutating func register<T>(_ dependency : T) {
dependencyList[ObjectIdentifier(T.self)] = dependency
}


private mutating func remove<T>(_ type: T.Type) {
dependencyList.removeValue(forKey: ObjectIdentifier(type))
}

private mutating func remove<T>(_ dependency: T) {
dependencyList.removeValue(forKey: ObjectIdentifier(T.self))
}

/// Singleton instance of the DependencyInjector.
internal static var shared = DependencyInjector()
private init() { }
Expand Down
27 changes: 10 additions & 17 deletions Tests/Injection/MainActorResolutionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@ import Testing
@testable import Injection

@MainActor
class MainActorResolutionTests {
struct MainActorResolutionTests {

init() {

}

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

@Test func testDependencyProviderInline() async throws {
final class MyTestDependency: Sendable {}
final class MySecondDependency: Sendable {}

// Register dependencies
let providedDependency = MyTestDependency()
DependencyInjector.register(providedDependency)
Expand All @@ -27,6 +24,9 @@ class MainActorResolutionTests {
}

@Test func testDependencyProviderPropertyWrapper() async throws {
final class MyTestDependency: Sendable {}
final class MySecondDependency: Sendable {}

// Register dependencies
let providedDependency = MyTestDependency()
DependencyInjector.register(providedDependency)
Expand All @@ -40,6 +40,9 @@ class MainActorResolutionTests {
}

@Test func expectNilForResolveWithoutRegistration() async throws {
final class MyTestDependency: Sendable {}
final class MySecondDependency: Sendable {}

let dependency: MyTestDependency? = DependencyInjector.safeResolve()
#expect(dependency == nil)

Expand All @@ -51,13 +54,3 @@ class MainActorResolutionTests {
#expect(resolvedDependency === providedDependency)
}
}

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

}

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

}
32 changes: 10 additions & 22 deletions Tests/Injection/NonMainActorResolutionTests.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import Testing
@testable import Injection

class NonMainActorResolutionTests {

init() {

}

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

struct NonMainActorResolutionTests {
@Test func testDependencyProviderInline() async throws {
final class MyTestDependency: Sendable {}
final class MySecondDependency: Sendable {}

// Register dependencies
let providedDependency = MyTestDependency()
await DependencyInjector.register(providedDependency)
Expand All @@ -26,6 +18,9 @@ class NonMainActorResolutionTests {
}

@Test func testDependencyProviderPropertyWrapper() async throws {
final class MyTestDependency: Sendable {}
final class MySecondDependency: Sendable {}

// Register dependencies
let providedDependency = MyTestDependency()
await DependencyInjector.register(providedDependency)
Expand All @@ -39,6 +34,9 @@ class NonMainActorResolutionTests {
}

@Test func expectNilForResolveWithoutRegistration() async throws {
final class MyTestDependency: Sendable {}
final class MySecondDependency: Sendable {}

let dependency: MyTestDependency? = await DependencyInjector.safeResolve()
#expect(dependency == nil)

Expand All @@ -50,13 +48,3 @@ class NonMainActorResolutionTests {
#expect(resolvedDependency === providedDependency)
}
}

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

}

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

}
25 changes: 4 additions & 21 deletions Tests/Injection/ProtocolResolvementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,11 @@ import Testing
@testable import Injection

@MainActor
class ProtocolResolvementTests {

init() {

}

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

struct ProtocolResolvementTests {
@Test func testProtocolResolve() async throws {
protocol MyProtocol: AnyObject {}
final class MyProtocolImplementation: MyProtocol {}

let providedDependency = MyProtocolImplementation()
DependencyInjector.register(providedDependency, as: MyProtocol.self)

Expand All @@ -23,12 +15,3 @@ class ProtocolResolvementTests {
#expect(resolvedDependency === providedDependency)
}
}


fileprivate protocol MyProtocol: AnyObject {

}

fileprivate final class MyProtocolImplementation: MyProtocol {

}
Loading