Hi Swift community! ![]()
I’d like to introduce Service, a lightweight dependency injection (DI) library for modern Swift projects.
Service is inspired by Swinject (familiar container ergonomics) and swift-dependencies (modern dependency management/modeling ideas).
It aims to be a Swift 6 Concurrency-friendly alternative to Swinject: you keep the familiar register/resolve mental model and an engineering-friendly Assembly workflow, while adopting patterns that fit async/actors/MainActor UI naturally.
Repo + Docs:
- GitHub: GitHub - nslogmeng/swift-service: A lightweight, zero-dependency, type-safe dependency injection framework for modern Swift.
- Docs : Documentation
Highlights
- Lightweight & zero-dependency: easy to adopt, easy to ship.
- Concurrency-oriented (Swift 6): designed for modern async code, actors, and explicit MainActor UI dependencies.
- Familiar for Swinject users:
register/resolve+ Assembly-style organization for large apps. - Test-friendly: override dependencies cleanly (without global container mutation) using TaskLocal-based environments.
Familiar API: register / resolve
import Service
protocol DatabaseProtocol { /* ... */ }
ServiceEnv.current.register(DatabaseProtocol.self) {
DatabaseService(connectionString: "sqlite://app.db")
}
Injection stays clean at call sites:
struct UserRepository {
@Service var database: DatabaseProtocol
func fetchUser(id: String) async throws -> User? {
try await database.findUser(id: id)
}
}
Assembly style for modular apps (Swinject-friendly)
This is the pattern I’m using in a real project today:
struct AppAssembly: ServiceAssembly {
func assemble(env: ServiceEnv) {
env.registerMain(AppContainer.self) { AppContainer() }
env.register(Localization.self) { Localization() }
env.register(ThemeManager.self) { ThemeManager() }
env.registerMain(Router.self) { Router() }
env.registerMain(Overlay.self) { Overlay() }
}
}
Bootstrap:
ServiceEnv.current.assemble([
AppAssembly()
])
This scales well when you split registrations by features/modules and keep composition explicit.
MainActor-safe dependencies (SwiftUI / UI)
UI-facing services (routers, overlays, view models, etc.) often must live on MainActor.
Service provides a dedicated pathway so intent is explicit:
ServiceEnv.current.registerMain(Router.self) { Router() }
struct ContentView: View {
@MainService var router: Router
var body: some View { /* ... */ }
}
Testing: switch environment and assemble the same modules
In tests you can switch to a .test environment at the outermost scope and keep the same Assembly structure:
await ServiceEnv.$current.withValue(.test) {
ServiceEnv.current.assemble([
AppAssembly()
// ... other assemblies
])
// Run your test logic inside the .test environment
}
And inside an Assembly you can branch on the environment to register mocks while keeping registrations structured:
struct AppAssembly: ServiceAssembly {
func assemble(env: ServiceEnv) {
if env.isTest {
env.register(Localization.self) { MockLocalization() }
} else {
env.register(Localization.self) { Localization() }
}
// keep the rest identical
env.register(ThemeManager.self) { ThemeManager() }
}
}
If you’re using Swinject and want a Swift 6 Concurrency-friendly DI with zero dependencies and a familiar workflow, I’d love for you to try Service — feedback, edge cases, issues, and PRs are very welcome!
Thanks for reading!