dependence is a Swift 6.3+ dependency-injection package built around typed
dependency keys, Sendable witness values, and @TaskLocal scoped overrides.
It is designed for SwiftPM-first apps that want explicit composition roots,
parallel-safe tests, preview-safe defaults, and platform-native bridges for
SwiftUI, UIKit, and AppKit.
The core product has no third-party runtime dependency. The optional macro
product depends on swiftlang/swift-syntax only at build time.
- Swift tools version: 6.3
- Swift language mode: 6
- Apple platforms declared by the package: iOS 26, macOS 26, tvOS 26, watchOS 26, visionOS 26
- Linux: supported by the core and testing targets where the Swift toolchain provides the required standard library modules
| Product | What it contains |
|---|---|
Dependence |
Core DependencyValues, DependencyKey, @Dependency, withDependencies, prepareDependencies, Provider, Lazy, ScopeToken, issue reporting, and the conditional SwiftUI bridge. |
DependenceMacros |
Optional macros: @DependencyEntry, @DependencyClient, and @Dependencies. Re-exports Dependence. |
DependenceTesting |
Swift Testing integration, .dependencies { } traits, TestClock, ImmediateClock, and UnimplementedClock. |
DependenceUIKit |
UIKit trait-chain storage, UIViewController.dependencies, UIView.dependencies, and observation helpers. |
DependenceAppKit |
AppKit responder-chain lookup and NSDocument-scoped dependency storage. |
// Package.swift
dependencies: [
.package(url: "https://github.com/Aemi-Studio/dependence.git", branch: "main"),
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "Dependence", package: "dependence"),
.product(name: "DependenceMacros", package: "dependence"), // optional
]
),
.testTarget(
name: "MyAppTests",
dependencies: [
"MyApp",
.product(name: "DependenceTesting", package: "dependence"),
]
),
]Import only what each target needs:
import Dependence
import DependenceMacros // only in targets that use macros
import DependenceTesting // tests only
import DependenceUIKit // UIKit adapters
import DependenceAppKit // AppKit adaptersDeclare service clients as Sendable structs of @Sendable closures. This
"witness" shape makes live, preview, and test implementations ordinary values.
import Dependence
import DependenceMacros
@DependencyClient
public struct APIClient: Sendable {
public var fetchGreeting: @Sendable () async throws -> String
}
extension APIClient {
public static let live = APIClient(
fetchGreeting: { "hello, world" }
)
public static let preview = APIClient(
fetchGreeting: { "hello from preview" }
)
}
extension DependencyValues {
@DependencyEntry(
preview: APIClient.preview,
test: APIClient.unimplemented
)
public var apiClient: APIClient = .live
}Read the value with @Dependency:
import Dependence
import DependenceMacros
import Observation
@MainActor
@Observable
@Dependencies(\.apiClient)
final class GreetingViewModel {
private(set) var greeting = ""
func load() async throws {
greeting = try await apiClient.fetchGreeting()
}
}Override it for a lexical task scope:
try await withDependencies {
$0.apiClient = APIClient(fetchGreeting: { "from test" })
} operation: {
let model = await MainActor.run { GreetingViewModel() }
try await model.load()
}Every dependency is identified by a key type. A key supplies default values for runtime, previews, and tests:
enum APIClientKey: DependencyKey {
static var liveValue: APIClient { .live }
static var previewValue: APIClient { .preview }
static var testValue: APIClient { .unimplemented }
}
extension DependencyValues {
var apiClient: APIClient {
get { self[APIClientKey.self] }
set { self[APIClientKey.self] = newValue }
}
}@DependencyEntry writes that boilerplate for the common case. The manual form
remains useful when the macro convention does not fit.
DependencyValues is a Sendable value type. Explicit overrides live in the
current DependencyValues instance. Default values are resolved lazily and
cached process-wide by dependency key and execution context.
For a full DependencyKey, defaults resolve as follows:
| Context | Default value |
|---|---|
| App/runtime | liveValue |
| SwiftUI preview process | previewValue |
| Swift Testing | testValue |
| XCTest | testValue |
For TestDependencyKey, which is used by interface-only modules that cannot
see a live implementation, defaults resolve as follows:
| Context | Default value |
|---|---|
| App/runtime | testValue |
| SwiftUI preview process | previewValue |
| Swift Testing | testValue |
| XCTest | testValue |
Fallbacks are inherited from the protocols:
TestDependencyKey.previewValuedefaults totestValue.DependencyKey.testValuedefaults toliveValue.- Therefore, a bare
DependencyKeywith onlyliveValueusesliveValuein runtime, preview, and test contexts.
The preview detector checks XCODE_RUNNING_FOR_PREVIEWS == "1" before test
framework probes so Xcode previews get previewValue even if XCTest is loaded
by the preview host.
withDependencies copies the currently bound task-local values, applies your
mutations, and binds that copy for the duration of the operation.
withDependencies {
$0.apiClient = .preview
} operation: {
// Synchronous reads see .preview here.
}
await withDependencies {
$0.apiClient = .preview
} operation: {
// Structured child tasks inherit the override.
}Nested overrides compose. Inner mutations shadow outer mutations for the same key while inheriting all other keys.
Structured concurrency inherits overrides automatically:
async letinherits.withTaskGroup.addTaskinherits.Task.detached, GCD, Combine callbacks, and NotificationCenter callbacks do not inherit.
Use captureDependencies() immediately before crossing an escaping boundary:
let continuation = captureDependencies()
DispatchQueue.global().async {
continuation.yield {
// Reads are rebound to the captured dependency values.
}
}captureDependencies() captures DependencyValues.current. That includes an
active task-local override, or the latest .dependencies { } SwiftUI subtree
fallback when no task-local override is active. It does not inspect arbitrary
SwiftUI @Environment values by itself.
Dependence conditionally bridges into SwiftUI when SwiftUI is available.
At the app composition root, use the scene modifier:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup { RootView() }
.dependencies {
$0.apiClient = .live
}
}
}The first scene evaluation seeds the process-wide default cache. Later scene reevaluations are silent no-ops for the global install, so treat this as a composition-root API, not a dynamic reconfiguration mechanism.
For previews, feature flags, and local branches, use the view modifier:
RootView()
.dependencies {
$0.apiClient = .preview
}The view modifier writes the override into SwiftUI EnvironmentValues and also
publishes a subtree entry so non-View hosts, such as @Observable view models,
can resolve the same override.
Resolution precedence depends on where the read happens:
| Read site | Precedence |
|---|---|
@Dependency installed on a SwiftUI View or ViewModifier |
SwiftUI environment override, then task-local override, then subtree fallback, then defaults. |
@Dependency on a non-View host |
Task-local override, then latest SwiftUI subtree fallback, then defaults. |
DependencyValues.current |
Task-local override, then latest SwiftUI subtree fallback, then defaults. |
Direct subscript on a specific DependencyValues value |
That value's overrides, then cached/context defaults. |
Empty override containers are ignored in the environment/subtree fallback path.
For Xcode previews, DependencePreview wraps the view modifier in Apple's
PreviewModifier shape:
#Preview(traits: .modifier(DependencePreview { $0.apiClient = .preview })) {
RootView()
}If a key has @DependencyEntry(preview: ...), previews can often rely on the
automatic previewValue without any modifier.
For non-SwiftUI apps, or for apps that prefer explicit startup wiring, call
prepareDependencies once at the very beginning of the process:
@main
enum AppMain {
static func main() async throws {
prepareDependencies {
$0.apiClient = .live
}
try await run()
}
}The first call installs the supplied values into the process-wide cache for all execution contexts. A second call reports an issue and is ignored. Configure all live dependencies in one composition root.
TestDependencyKey lets an interface module declare a dependency slot without
importing the live implementation.
// AuthInterface
public enum AuthClientKey: TestDependencyKey {
public static var testValue: AuthClient { .unimplemented }
public static var previewValue: AuthClient { .preview }
}
extension DependencyValues {
@DependencyEntry public var authClient: AuthClient
}The no-initializer macro form routes through a key named from the value type:
AuthClient -> AuthClientKey. The implementation module then adds the live
conformance:
// AuthImpl
extension AuthClientKey: DependencyKey {
public static var liveValue: AuthClient { .live }
}Only the app target imports AuthImpl and wires .live. Feature modules can
depend only on AuthInterface.
DependenceMacros is optional. All generated code is ordinary Swift that can be
written manually.
With an initializer:
@DependencyEntry(preview: APIClient.preview, test: APIClient.unimplemented)
public var apiClient: APIClient = .liveThe macro generates:
- A fileprivate
__Key_apiClienttype conforming toDependencyKey. liveValuefrom the initializer expression.- Optional
previewValueandtestValuewitnesses from labeled arguments. - A get/set accessor routed through
self[__Key_apiClient.self].
Without an initializer:
@DependencyEntry public var authClient: AuthClientThe macro generates accessors routed through self[test: AuthClientKey.self].
The external key must conform to TestDependencyKey, and a live module may
later conform it to DependencyKey.
Use on a struct of closure properties:
@DependencyClient
public struct SearchClient: Sendable {
public var search: @Sendable (String) async throws -> [String]
public var cancel: @Sendable () -> Void
}The macro generates a memberwise initializer. Closure parameters default to unimplemented closures:
- Throwing closures report an issue and throw
DependencyError.unimplemented. Voidclosures report an issue and return.- Non-throwing, non-
Voidclosures report an issue and then trap because there is no value to return. The macro emits a warning for this shape; preferthrowsif the unimplemented path should be recoverable in tests. - Non-closure stored properties become required initializer parameters.
static var unimplementedis generated only when every stored property can be defaulted.
The synthesized init and static var unimplemented are always emitted with
the nonisolated keyword. This is what makes @DependencyClient usable from
modules built with SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor (the Xcode 26
default-isolation build setting). Without nonisolated, every declaration in
such a module — including the macro-synthesized initializer and the closure
defaults it bakes into the init signature — would be inferred @MainActor,
and any preview/test witness mounted off the main actor would fail to compile:
// In a module compiled with -default-isolation MainActor:
@DependencyClient
struct SearchClient: Sendable {
var search: @Sendable (String) async throws -> [String]
}
extension SearchClient {
// Without the `nonisolated init` synthesis this fails with:
// error: main actor-isolated default value in a nonisolated context
nonisolated static let preview = Self(
search: { _ in ["preview"] }
)
}The witness is conceptually Sendable — its init only assigns Sendable
closures to stored properties — so the nonisolated stamp is sound by
construction. The compiler still enforces actor isolation on the closure
bodies the caller supplies, so this does not weaken any guarantee at the
call site.
Use on @Observable view models and similar classes:
@Dependencies(\.authClient, \.feedClient)
final class HomeViewModel {}The macro generates private stored properties:
@ObservationIgnored
@Dependence.Dependency(\.authClient) private var authClientEach key path must contain exactly one property component, such as
\.authClient. Duplicate key paths are skipped.
DependenceTesting integrates with Swift Testing:
import Dependence
import DependenceTesting
import Testing
@Suite(.dependencies { $0.apiClient = .preview })
struct GreetingTests {
@Test(.dependencies { $0.apiClient = APIClient(fetchGreeting: { "test" }) })
func greets() async throws {
@Dependency(\.apiClient) var api
#expect(try await api.fetchGreeting() == "test")
}
}Suite traits are recursive. Test-level traits layer on top of suite-level traits, and the inner mutation wins on conflicts.
DependenceTesting also installs Swift Testing issue routing. Once a testing
API from this product is touched, reportIssue calls made inside a running
@Test are recorded with Issue.record.
TestClock is deterministic. Sleeps suspend until the test advances the clock.
let clock = TestClock()
async let value: Void = clock.sleep(for: .seconds(1))
await clock.advance(by: .seconds(1))
try await valueadvance(by:) and advance(to:) resume sleepers whose deadlines have passed,
in deadline order. run() drains all pending sleepers. Cancellation resumes
sleepers with CancellationError.
ImmediateClock never actually sleeps. It advances its local now to the
requested deadline, yields once, and honors cancellation.
UnimplementedClock reports an issue whenever now, minimumResolution, or
sleep is used. It is intended as a test default for clock dependencies.
DependenceUIKit stores DependencyValues in the UIKit trait chain:
containerViewController.dependencies {
$0.apiClient = .preview
}
let values = view.traitCollection.dependenciesUIViewController.dependencies and UIView.dependencies start from currently
inherited trait values, apply your mutation, and write the result to
traitOverrides.dependencies.
For explicit Observation tracking, use:
withObservedDependencies({ values in
values.apiClient
}, onChange: {
view.setNeedsLayout()
})The helper uses withObservationTracking and invokes onChange once on the
main actor. Re-arm it after a change if you need continuous observation.
DependenceAppKit uses the responder chain:
@MainActor
final class WindowController: NSWindowController, DependencyHosting {
var dependencies = DependencyValues()
}
let values = someResponder.inheritedDependenciesinheritedDependencies walks nextResponder until it finds a
DependencyHosting responder, then returns that host's values. If no host is
found, it returns an empty container.
NSDocument conforms to DependencyHosting through associated-object storage,
so document-based apps can scope dependencies per document.
Use Provider<Value> for "make a fresh value every time":
struct LoginClient: Sendable {
var makeAttempt: Provider<LoginAttempt>
}Use AsyncProvider<Value> for async factories.
Use Lazy<Value> for "initialize on first use and cache":
let expensive = Lazy { ExpensiveClient() }
let client = expensive()Lazy computes outside its lock so dependencies can be read during
construction without deadlocking. Under contention, more than one caller may
run the producer closure, but only the first installed value is stored and
returned thereafter. Keep the producer side-effect-safe.
Use ScopeToken<Tag, Value> for single-use generational scopes such as a
post-login session:
enum SessionScope: ScopeTag {}
let session = ScopeToken<SessionScope, User>(
value: user,
teardown: { print("session ended") }
)
await session.enter { borrowed in
let user = borrowed.snapshot()
await withDependencies {
$0.currentUser = user
} operation: {
await runAuthenticatedShell()
}
}ScopeToken is ~Copyable. The compiler rejects copies and use after consume.
enter runs teardown whether the operation returns or throws. close() consumes
the token and runs teardown without running an operation.
reportIssue is used for unimplemented sentinels and recoverable
misconfigurations.
Routing is context-aware:
| Context | Sink |
|---|---|
Swift Testing with DependenceTesting bootstrapped |
Issue.record |
| Swift Testing without a registered handler | runtime warning |
| XCTest | [XCTest]-prefixed runtime warning |
| SwiftUI preview or runtime | runtime warning |
On Apple platforms, runtime warnings use os.Logger. In debug builds they are
logged as faults so Xcode surfaces them prominently. On non-Apple platforms,
warnings are written to stderr.
The package includes executable examples:
| Target | Demonstrates |
|---|---|
ExampleSmallApp |
A single SwiftUI app with @Dependency, @Dependencies, preview defaults, and subtree overrides. |
ExampleModularApp |
Interface/implementation/test-support module split for Auth, Feed, and Profile features. |
ExampleSessionApp |
ScopeToken for a post-login session lifetime. |
ExampleStressApp |
A 20-key dependency registry, macro-heavy registration, nested overrides, graph walking, and benchmark hooks. |
Build an example with:
swift build --product ExampleSmallAppRun stress benchmarks with:
Tools/stress-profile.shdependence guarantees typed key-path access, Sendable dependency storage,
parallel-safe task-local overrides, deterministic test traits, and native
SwiftUI/UIKit/AppKit integration.
It does not perform whole-program graph validation. A key path proves the slot exists; it does not prove every app composition root remembered to install a live value. Use interface/implementation module boundaries, unimplemented test defaults, and focused tests to keep that honest.
Detached tasks and callback APIs do not inherit task-local values. Capture and
rebind explicitly with captureDependencies().
Non-Sendable services should be wrapped behind actors, isolated to the main
actor, or represented by Sendable witnesses. Avoid storing arbitrary
non-thread-safe reference types directly in DependencyValues.
Different APIs answer different questions about time — when a value enters
the system, when it is sampled, when it leaves, and what happens if you try
to swap it. The full contract — including the hotload matrix per read site,
identity-warning for non-View hosts, and the recommended snapshot-at-
construction pattern for long-lived view models — lives in the DocC article
Lifetime.md.
Quick reference:
prepareDependencies/Scene.dependencies— process-lifetime, first-call-wins.View.dependencies— subtree-lifetime, hotloadable forViewreads.withDependencies— task-local, hotloadable for the operation.captureDependencies—Sendablesnapshot for crossing escaping boundaries.Provider— fresh per call (factory body decides freshness).Lazy— one-shot, not hotloadable.ScopeToken— generational lifetime with deterministic teardown.
DocC documentation starts at
Sources/Dependence/Resources/Documentation.docc/Dependence.md, with the
behavior reference in
Sources/Dependence/Resources/Documentation.docc/Behavior.md and the
lifetime/hotload contract in
Sources/Dependence/Resources/Documentation.docc/Lifetime.md.
The files in docs/artifact_*.md are historical design research. They are
useful background, but the README and DocC pages are the canonical description
of the implemented package behavior.