Crocodil is a dependency injection (DI) library for Swift that provides a straightforward, boilerplate-free way to register and access dependencies in your applications.
Dependency Injection is a design pattern that implements Inversion of Control (IoC) to decouple component dependencies. Crocodil offers an elegant macro-powered approach to DI in Swift.
-
Tight Coupling Without DI, components often create dependencies directly, leading to tight coupling. Crocodil promotes loose coupling, making components easier to test, reuse, and maintain.
-
Initializer Injection Not Always Possible Scenarios where initializer-based DI doesn't work:
- Interfacing with system or third-party frameworks where initializers can't be modified
- Singleton objects that manage their own instantiation
- Legacy codebases with rigid construction patterns
- Testing Complexity DI enables easy mocking and stubbing. Crocodil makes swapping dependencies effortless for testing purposes.
-
Inject Anything. Supports injection of enums, structs, classes, closures, and protocol conforming instances
-
Macro-powered Simplicity. With @DependencyEntry`, Crocodil uses Swift macros to register and declare dependencies in one place.
-
Compile-time Safety. Ensures key-path validity and detects missing dependencies during compilation.
-
Swift Concurrency Compliant. Drop-in replacement for singletons, without triggering strict concurrency mode violations.
-
Clean Property Injection. Uses @Dependency propery wrapper for clean and read-only dependency access.
-
Thread Safety. Built-in concurrency support with safe, synchronized access to dependencies.
Using Swift Package Manager:
.package(url: "https://github.com/KazaiMazai/Crocodil.git", from: "0.1.0")Or via Xcode:
- File → Add Packages
- Enter the URL:
https://github.com/KazaiMazai/Crocodil.git
Declaration and registration happen in one shot. The variable name then acts as a keypath-based key ensuring compile-time completeness:
extension Dependencies {
// Register protocol implementation
@DependencyEntry var networkClient: ClientProtocol = NetworkClient()
// Register shared instance
@DependencyEntry var userDefaultsStorage = UserDefaults.standard
// Register lazily initialized instance
@DependencyEntry var lazyService = { Service() }()
// Register closure
@DependencyEntry var currentTime: @Sendable () -> Date = { Date.now }
var now: Date { currentTime() }
}Use @Dependency to inject dependencies via key paths:
@Observable
class ViewModel {
@Dependency(\.networkClient) var client
@Dependency(\.userDefaultsStorage) var storage
@Dependency(\.now) var now
}Or access dependencies directly:
let currentTime = Dependency[\.now]Swap out dependencies at runtime, perfect for unit tests:
Dependencies.inject(\.networkClient, NetworkClientMock())
Dependencies.inject(\.currentTime, { Date.distantPast })Crocodil allows to mutate dependencies atomically:
extension Dependencies {
@DependencyEntry var intValue: Int = 0
}
Dependencies.update(intValue: {
$0 += 1
})
Note
Due to Swift macro limitations, atomic mutation is availave only for dependencies injected with explicitly declared type.
Declare type explicitly to enable code generation of atomic mutation func:
extension Dependencies {
- @DependencyEntry var intValue = 0
+ @DependencyEntry var intValue: Int = 0
}Crocodil respects access control attributes allowing to naturally scope the dependencies instances. This will create and register 2 independent instances of dependencies. Each will be accessed according to access control attributes:
//FileA.swift:
fileprivate extension Dependencies {
@DependencyEntry var localService = Service()
}
//FileB.swift:
fileprivate extension Dependencies {
@DependencyEntry var localService = Service()
} @DependencyEntry supports all kinds of types including closures and lazy instance initialization. This allows to design any kind of dependency lifecycle:
extension Dependencies {
// Plain singleton
@DependencyEntry var networkClient: ClientProtocol = NetworkClient()
// Lazily initialized singleton
@DependencyEntry var lazyService = { Service() }()
// Transient instance via closure
@DependencyEntry var now = { Date() }
} Crocodil supports creating custom dependency containers beyond the main Dependencies container. This is useful for organizing dependencies by feature, module, or any logical grouping.
To create a custom container, create a struct that conforms to the Container protocol:
struct AppFeatures: Container {
init() { }
@DependencyEntry var newOnboarding: Bool = false
@DependencyEntry var experimentalFeature: Bool = true
@DependencyEntry var analyticsEnabled: Bool = true
}Create typealias to access dependencies from custom container via InjectableKeyPath:
typealias Feature<Value> = InjectableKeyPath<AppFeatures, Value>
class ViewModel {
@Feature(\.newOnboarding) var newOnboarding
}Or access dependencies directly using subscript syntax:
let isNewOnboarding = Feature[\.newOnboarding]Replace dependencies at runtime, just like with the main container:
AppFeatures.inject(\.newOnboarding, true)- Feature Flags: Group feature flags and experimental features
- Module-specific Dependencies: Separate dependencies by app modules
- Testing: Create test-specific containers for mock instances
// Test container for accessing mock instances
struct MockDependencies: Container {
init() { }
@DependencyEntry var networkClient: NetworkClientProtocol = { MockNetworkClient() }()
@DependencyEntry var analytics: AnalyticsProtocol = { MockAnalytics() }()
}
typealias Mock<Value> = InjectableKeyPath<MockDependencies, Value>
//Inject before testing:
Dependencies.inject(\.networkClient, Mock[\.networkClient])
Dependencies.inject(\.networkClient, Mock[\.analytics])Replace your good old singletons with Swift 6 strict concurrency compatible alternative and never deal with nasty
Static property 'shared' is not concurrency-safe because it is nonisolated global shared mutable state again:
extension Dependencies {
+ @DependencyEntry var networkClient: ClientProtocol = NetworkClient()
}
class NetworkClient {
- static let shared: NetworkClient = NetworkClient()
+ static var shared: NetworkClient { Dependency[\.networkClient] }
}Crocodil provides a workaround to silence the Swift 6 concurrency warning by using nonisolated(unsafe) and syncronizes access to the variable via dedicated concurrent queue which makes access to the shared variable actually safe.
Crocodil is designed in a way to make it impossible to access or mutate the global var directly in any unsafe way.
Warning
Although access to the dependencies is syncronized and is thread-safe it doesn't make the dependencies themselves thread-safe or sendable. It's developer's respinsibiliy to make the injected things' internal state thread-safe.
| Feature | SwiftUI EnvironmentValues | Crocodil Injection |
|---|---|---|
| Context | SwiftUI-only | Framework-agnostic |
| Propagation | Passed down view hierarchy | Globally accessible |
| View Re-rendering | Triggers updates | Does not trigger updates |
| Keys Mechanism | Uses EnvironmentKey |
Uses DependencyKey |
| Macro | @Entry |
@DependencyEntry |
| Thread Safety | Limited | Built-in concurrent safety |
Crocodil cannot detect circular references. Accessing Dependency within another dependency declaration should be made with extra care or avoided.
extension Dependencies {
// Be aware of circular references. They are possible and will lead to a crash:
@DependencyEntry var service = Service(Dependency[\.anotherService])
@DependencyEntry var anotherService = AnotherService(Dependency[\.service])
}While read/write access to the injected instances is synchronized, the injected instances themselves are not automatically thread-safe.
There are many dependency injection libraries in the Swift, but only one of them is Crocodil
This library is released under the MIT license. See LICENSE for details.