← Back to list

@Injected Property Wrapper in Swift: Clean Dependency Injection Without the Mess

Introduction

Константин Клинов · 2026-03-09 03:45 · 0 claps · 2.1 min read
#swift #iosdev #swiftui #dependency-injection #property-wrapper
Open on Medium ↗
Wiki topics: 📱 · Mobile Development 🎵 · Music & Audio

@Injected Property Wrapper in Swift: Clean Dependency Injection Without the Mess

Introduction

Dependency Injection (DI) is one of those concepts that every iOS developer eventually encounters. It sounds complex, but the core idea is simple: instead of a class creating its own dependencies, they are provided from the outside. This makes code modular, testable, and maintainable.

Swift’s [@propertyWrapper](http://twitter.com/propertyWrapper) feature, introduced in Swift 5.1, lets us build elegant DI solutions — and the [@Injected](http://twitter.com/Injected) pattern is one of the most powerful results.

The Problem: Constructor Injection Hell

Without DI tooling, you often end up with this:

class ProfileViewController: UIViewController {
 let viewModel: ProfileViewModel
init(viewModel: ProfileViewModel) {
 self.viewModel = viewModel
 super.init(nibName: nil, bundle: nil)
 }
}

class ProfileViewModel {
 let networkService: NetworkService
 let analyticsService: AnalyticsService
init(networkService: NetworkService, analyticsService: AnalyticsService) {
 self.networkService = networkService
 self.analyticsService = analyticsService
 }
}

Every layer has to pass dependencies down. It becomes unwieldy fast.

The Solution: @Injected Property Wrapper

Here’s how to build and use the [@Injected](http://twitter.com/Injected) pattern from scratch.

Step 1: Define Your Protocols

protocol NetworkService {
 func fetchUser(id: String) async throws -> User
}
protocol AnalyticsService {
 func track(event: String)
}

Step 2: Create a DI Container

final class DIContainer {
 static let shared = DIContainer()
 private var registry: [String: Any] = [:]
private init() {}
func register<T>(_ type: T.Type, factory: @escaping () -> T) {
 registry[String(describing: type)] = factory
 }
func resolve<T>(_ type: T.Type) -> T {
 let key = String(describing: type)
 guard let factory = registry[key] as? () -> T else {
 fatalError("Dependency not registered: \(key)")
 }
 return factory()
 }
}

Step 3: Build the @Injected Property Wrapper

@propertyWrapper
struct Injected<T> {
 private var value: T
init() {
 self.value = DIContainer.shared.resolve(T.self)
 }
var wrappedValue: T {
 get { value }
 set { value = newValue }
 }
}

Step 4: Register Dependencies at App Launch

@main
struct MyApp: App {
 init() {
 setupDependencies()
 }
func setupDependencies() {
 DIContainer.shared.register(NetworkService.self) {
 RealNetworkService()
 }
 DIContainer.shared.register(AnalyticsService.self) {
 FirebaseAnalyticsService()
 }
 }
var body: some Scene {
 WindowGroup {
 ContentView()
 }
 }
}

Step 5: Enjoy Clean Injection Everywhere

class ProfileViewModel: ObservableObject {
 @Injected var networkService: NetworkService
 @Injected var analyticsService: AnalyticsService
@Published var user: User?
func loadProfile(id: String) async {
 analyticsService.track(event: "profile_view")
 user = try? await networkService.fetchUser(id: id)
 }
}

No constructor arguments. No prop drilling. Pure, readable code.

Bonus: Swapping for Tests

class MockNetworkService: NetworkService {
 func fetchUser(id: String) async throws -> User {
 return User(id: "mock", name: "Test User")
 }
}
DIContainer.shared.register(NetworkService.self) {
 MockNetworkService()
}

Swapping the entire implementation for testing takes one line.

Lazy Resolution Variant

If you want to defer resolution until the property is first accessed:

@propertyWrapper
struct LazyInjected<T> {
 private var value: T?
var wrappedValue: T {
 mutating get {
 if value == nil {
 value = DIContainer.shared.resolve(T.self)
 }
 return value!
 }
 }
}

Real-World Libraries Using This Pattern

  • Resolver by Michael Long — [@Injected](http://twitter.com/Injected) out of the box
  • Swinject — powerful container with scoping and assembly
  • Factory — Swift-first, type-safe DI with [@Injected](http://twitter.com/Injected) support

Conclusion

The [@Injected](http://twitter.com/Injected) property wrapper is a small but powerful abstraction. It separates the what from the how, keeps your classes clean, and makes testing a breeze. Whether you roll your own or use a library, this pattern is worth adding to your Swift toolkit.

Support me on Patreon! If you found this helpful, consider supporting me on Patreon. Your support helps me create more content like this! 🙏 https://www.patreon.com/c/Kanstantsin733


메타데이터
post_id
cf78a4e6e0ea
slug
injected-property-wrapper-in-swift-clean-dependency-injection-without-the-mess-cf78a4e6e0ea
url
https://medium.com/@kost9klinov/injected-property-wrapper-in-swift-clean-dependency-injection-without-the-mess-cf78a4e6e0ea
canonical_url
https://medium.com/@kost9klinov/injected-property-wrapper-in-swift-clean-dependency-injection-without-the-mess-cf78a4e6e0ea
author_url
https://medium.com/@kost9klinov
status
ok
fetched_at
2026-09-02 06:49:20