Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DIKit

Lightweight DI. Lifetime is required at registration. Preview and test values are selected automatically.

iOS 13+ · Swift 6


Installation

XcodeGen (project.yml)

packages:
  DIKit:
    url: https://github.com/Anatoli4/DIKit
    from: 1.0.0

targets:
  MyApp:
    dependencies:
      - package: DIKit
        product: DIKit
  MyAppTests:
    dependencies:
      - package: DIKit
        product: DIKitTesting

Swift Package Manager (from another package)

dependencies: [
    .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.


Registration

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)
}

Resolve

@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

.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()
}

Tests

.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.

Resolve order

  1. Launch argument (.onArg)
  2. register / withDependencies / prepareDependencies
  3. .preview / .test / .onDebug / simulator / device
  4. Live closure

Modules

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.

About

Lightweight DI.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages