Lightweight DI. Lifetime is required at registration. Preview and test values are selected automatically.
iOS 13+ · Swift 6
packages:
DIKit:
url: https://github.com/Anatoli4/DIKit
from: 1.0.0
targets:
MyApp:
dependencies:
- package: DIKit
product: DIKit
MyAppTests:
dependencies:
- package: DIKit
product: DIKitTestingdependencies: [
.package(url: "https://github.com/Anatoli4/DIKit", from: "1.0.0"),
],
targets: [
.target(
name: "MyApp",
dependencies: ["DIKit"]
),
.testTarget(
name: "MyAppTests",
dependencies: [
"MyApp",
.product(name: "DIKitTesting", package: "DIKit"),
]
),
]Add only DIKitTesting to the test target (it already depends on DIKit). Do not also link DIKit there — that duplicates containers.
Lifetime is required. Omitting it is a compile error.
import DIKit
extension Container {
var api: Dependency<APIClient> {
self(.singleton) { LiveAPIClient() }
.preview { PreviewAPIClient() }
.test { TestAPIClient() }
}
var analytics: Dependency<Analytics> {
self(.unique) { LiveAnalytics() }
.onDebug { NoOpAnalytics() }
}
}| Lifetime | Behavior |
|---|---|
.unique |
New instance on every resolve |
.cached |
One instance per container until the cache is reset |
.shared |
Weak: lives while something else holds a strong reference |
.singleton |
One instance per process (isolated in tests via @TaskLocal) |
.graph |
One instance per root resolve cycle |
custom Lifetime.Cached() |
Same as cached, reset independently |
extension Lifetime {
static let session = Cached()
}
extension Container {
var user: Dependency<User> {
self(.session) { User() }
}
}
func logout() {
Container.shared.manager.reset(lifetime: .session)
}@MainActor
@Observable
final class FeatureModel {
@ObservationIgnored
@Dependency(\.api) private var api
}
let api = Container.shared.api()
let api = dependency(\.api)@ObservationIgnored is required on @Dependency / @LazyDependency / @WeakLazyDependency / @DynamicDependency inside @Observable. Otherwise Observation tracks the wrapper storage and invalidates the UI unnecessarily. The attribute cannot be applied to the wrapper type — only to the property on the model.
@DependencyObservable is for a SwiftUI view that resolves an @Observable dependency.
.preview { } is used automatically in Xcode Previews. For a specific scenario:
#Preview {
let _ = prepareDependencies {
$0.api.register { FailingAPIClient() }
}
FeatureView()
}
#Preview("sugar") {
Container.shared {
$0.api.register { FailingAPIClient() }
}
FeatureView()
}.test { } is the default for XCTest and Swift Testing. An explicit register / withDependencies override takes precedence.
Resolving a live implementation in a test without .test and without an override is reported as a test issue (same idea as testValue in swift-dependencies).
import DIKit
import DIKitTesting
import Testing
@Suite(.container)
struct FeatureTests {
@Test func loads() {
Container.shared.api.register { MockAPIClient(.sample) }
let model = FeatureModel()
#expect(model.isLoaded)
}
}
@Test(.dependencies {
$0.api.register { MockAPIClient(.empty) }
})
func emptyState() {
#expect(FeatureModel().isEmpty)
}
let model = withDependencies {
$0.api.register { MockAPIClient(.error) }
} operation: {
FeatureModel()
}For XCTest: call Container.shared.reset() in setUp, or use withDependencies.
- Launch argument (
.onArg) register/withDependencies/prepareDependencies.preview/.test/.onDebug/ simulator / device- Live closure
Protocol in one module, implementation in another:
extension Container {
var loader: Dependency<AccountLoading?> { promised(.singleton) }
}
extension Container: @retroactive AutoRegistering {
public func autoRegister() {
loader.register { AccountLoader() }
}
}An unregistered promised dependency fatalErrors in DEBUG and returns nil in release.