All articles
iOS10 min read

Dependency Injection in Swift: A Beginner-to-Advanced Guide

🧪 Feeling tangled in singletons and tightly-coupled code? Learn how Dependency Injection in Swift can help you write cleaner, testable…

K
Karan Pal
Author
Image generated by AI
Image generated by AI

👋 Introduction:

So, you’ve stumbled into the world of Dependency Injection (DI) — welcome to the club where we don’t create objects like it’s 2009 anymore. 🎉

If you’ve ever written a service class, jammed it into a view controller, and thought, “Meh, I’ll just make it a singleton and move on…” — don’t worry, we’ve all been there. But as your project grows, these little shortcuts turn into big architectural regrets. 😬

That’s where Dependency Injection comes in. It’s not just a fancy term from enterprise software books — it’s a practical, test-friendly, and modular way to decouple your code. In this guide, we’ll break it down step-by-step, from the basics to the more advanced stuff like constructor injection, protocol-oriented DI, and using Swift’s property wrappers like a pro 🧑‍🔬.

And if you’re also figuring out how to structure your iOS app for the long haul, you might want to pair this article with this modular architecture guide — trust me, DI and modularity are like peanut butter and jelly 🍞🥜🍇.

**How to Structure a Scalable iOS App with Modular Architecture** _Struggling with messy codebases and slow builds? Learn how modular architecture can make your iOS app scalable…_medium.comhttps://medium.com/swift-pal/how-to-structure-a-scalable-ios-app-with-modular-architecture-b0130da83bca

💡 What is Dependency Injection?

At its core, Dependency Injection (DI) is just a fancy term for “giving an object what it needs, instead of letting it go find it.” Think of it like handing a chef the ingredients instead of making them run to the grocery store mid-recipe. 🧑‍🍳🛒

In Swift terms: Instead of your ViewController creating its own NetworkManager, you inject the NetworkManager from outside — like a civilized, responsible coder.

Here’s a basic example before we dive deeper:

class NetworkManager {
    func fetchData() {
        print("Fetching from API...")
    }
}

class HomeViewModel {
    let networkManager: NetworkManager

    // Dependency is injected via constructor
    init(networkManager: NetworkManager) {
        self.networkManager = networkManager
    }
}

Ta-da! That’s constructor injection. The HomeViewModel doesn’t care where the NetworkManager came from — it just happily uses it.

🤔 Why Bother?

You might ask, “But why go through all this injection drama when I can just do this?”

let networkManager = NetworkManager()

Fair question. But let’s look at what DI brings to the table:

**Understanding Clean Architecture in iOS: A Beginner’s Guide** _Tired of Spaghetti Code? 🍝 Let’s Clean Things Up in iOS!_medium.comhttps://medium.com/swift-pal/understanding-clean-architecture-in-ios-a-beginners-guide-69d09b4883c4

🧰 Types of Dependency Injection in Swift

There’s more than one way to inject a dependency — and no, we’re not talking about caffeine ☕. Swift supports multiple styles of DI, each with its own use case, pros, and quirks. Let’s explore them one by one with examples:

1\. 🔧 Constructor Injection (aka Initializer Injection)

The most common and recommended method — especially when the dependency is required for the object to function properly.

class UserService {
    func getUser() -> String {
        return "Taylor Swift"
    }
}

class ProfileViewModel {
    private let userService: UserService

    init(userService: UserService) {
        self.userService = userService
    }

    func loadUser() {
        print(userService.getUser())
    }
}

✅ Good for: Enforcing required dependencies, testability.

🚫 Not ideal when: The dependency is default and not mock friendly.

2\. 🪄 Property Injection

Injecting the dependency after the object is created — kind of like setting up your coffee after pouring the milk. Risky, but doable.

class AnalyticsService {
    func log(event: String) {
        print("Logged: \(event)")
    }
}

class SettingsViewModel {
    var analyticsService: AnalyticsService?

    func logUserAction() {
        analyticsService?.log(event: "User tapped settings")
    }
}

✅ Good for: Optional or late dependencies.

🚫 Downside: Less safe — nothing stops you from calling .log() before analyticsService is set.

3\. 🧙 Method Injection

Passing the dependency only when it’s needed, typically as a parameter to a function.

func trackLogin(using tracker: AnalyticsService) {
    tracker.log(event: "Login Successful")
}

✅ Good for: Stateless, one-off dependencies.

🚫 Not great when: You need the dependency in multiple places or throughout the class lifecycle.

4\. 🔁 Protocol-Oriented Injection

Injecting abstractions (protocols) instead of concrete classes. This is Swift’s favorite cocktail when you’re going Clean Architecture style 🍸.

protocol PaymentProcessor {
    func process()
}

class StripeProcessor: PaymentProcessor {
    func process() {
        print("Stripe payment processed.")
    }
}

class CheckoutViewModel {
    let processor: PaymentProcessor

    init(processor: PaymentProcessor) {
        self.processor = processor
    }

    func checkout() {
        processor.process()
    }
}

✅ Great for: Decoupling, mocking, and long-term testability.

🧠 Bonus: This is where Dependency Injection + Protocols + Clean Architecture becomes the ultimate trio. You’ll find this pattern a lot in this architecture breakdown.

**MVC vs MVVM vs VIPER in iOS: Which Architecture Should You Choose in 2025?** _In this no-fluff breakdown, we’ll compare the good, the bad, and the “wait… why is this so complex?” of each…_medium.comhttps://medium.com/swift-pal/mvc-vs-mvvm-vs-viper-in-ios-which-architecture-should-you-choose-in-2025-38386312e0c1

🚨 Common Mistakes and Gotchas in Dependency Injection

Dependency Injection is great, but like any powerful tool, it can be misused. Let’s look at the classic footguns developers often walk into — and how to sidestep them like a Swift ninja. 🥷🧼

1\. 🧱 Over-Injection (a.k.a. DI Overkill)

“Every class gets injected with 7 protocols, even the one that just sets a label.”

Don’t get carried away with injecting everything. Just because you can abstract a dependency doesn’t mean you should. If it doesn’t offer testability, flexibility, or a real separation of concerns — maybe just… don’t.

2\. 🌀 Ambiguous Ownership

You inject an object into 3 different classes, and now everyone’s modifying it like a group project with no leader.

Tip: Be mindful of reference types. If the object you’re injecting holds state (like a data cache), make sure it’s clear who owns it — otherwise, welcome to side-effect city 🌆.

3\. 🧊 Retain Cycles from Closures

Injected services that hold closures referencing self can trap you like a memory leak bear trap.

class ViewModel {
    var service: Service?

    func bind() {
        service?.onComplete = {
            self.doSomething() // ❌ Potential retain cycle
        }
    }
}

Fix: Use \[weak self\] unless you like watching Instruments crash your dreams.

4\. 🪞 Not Injecting Protocols

Injecting concrete types instead of protocols defeats the purpose. Your code becomes harder to test and less flexible.

// ❌ Injecting a concrete type
class ProfileViewModel {
    let userService: RealUserService

    init(userService: RealUserService) {
        self.userService = userService
    }
}

Now ProfileViewModel is tightly coupled to RealUserService. You can’t easily inject a mock or replace the implementation without refactoring the class itself.

Now, the better approach: inject a protocol instead.

// ✅ Abstracting with a protocol
protocol UserService {
    func getUser() -> String
}

class RealUserService: UserService {
    func getUser() -> String {
        return "Taylor Swift"
    }
}

class MockUserService: UserService {
    func getUser() -> String {
        return "Mock User"
    }
}

class ProfileViewModel {
    let userService: UserService

    init(userService: UserService) {
        self.userService = userService
    }
}
// In production
let viewModel = ProfileViewModel(userService: RealUserService())

// In tests
let testViewModel = ProfileViewModel(userService: MockUserService())

Boom! This is what true decoupling looks like. You’ve separated the what from the how — and that’s the real magic of dependency injection.

5\. 🧩 Random DI Strategy Mixing

Don’t randomly mix constructor injection, property injection, and service locators in the same class. It leads to chaos and inconsistency.

Stick to a pattern that fits your use case — and use it consistently.

⚔️ Service Locator vs Dependency Injection: Know the Difference

Ever heard someone say, “Yeah, we use Dependency Injection… we have a global ServiceManager.shared that gives us everything!”

Spoiler alert: that’s not DI — that’s a Service Locator, wearing a fake mustache. 🎭

🤖 So What’s a Service Locator?

A Service Locator is a centralized registry that holds and hands out dependencies when asked.

class ServiceLocator {
    static let shared = ServiceLocator()

    var analyticsService: AnalyticsTracking = FirebaseAnalytics()
    var authService: AuthService = AuthAPI()
}

Then you use it like this:

class LoginViewModel {
    let analytics = ServiceLocator.shared.analyticsService

    func login() {
        analytics.log(event: "Login from ViewModel")
    }
}

Looks convenient, right? But here’s the problem…

😬 Why It’s a Problem

Using a service locator creates hidden dependencies — your class looks like it doesn’t depend on anything, but secretly it’s pulling stuff from a global state.

Let’s compare:

✅ Dependency Injection

❌ Service Locator

✅ When Is Service Locator Acceptable?

Let’s be fair — it’s not all evil.

🔸 It can be okay for small projects or temporary hacks during prototyping.

🔸 It’s sometimes used under-the-hood by frameworks (like Swinject containers), where it’s controlled and explicit.

🔸 Some developers use it in combination with DI, only at the composition root (we’ll talk about this next if you’d like).

But for actual feature-level logic, prefer explicit injection — it’s cleaner, more testable, and your future self will high-five you. 🙌

✅ Wrapping Up: DI All the Way, Baby

Dependency Injection in Swift isn’t just some over-engineered academic concept — it’s a battle-tested way to write cleaner, more testable, and more maintainable code. Whether you’re building a login screen or architecting a modular beast of an app, DI has your back 💪

Here’s your TL;DR:

If this article got your mental gears turning, you’ll definitely want to check out these architecture deep-dives:

👉 How to Structure a Scalable iOS App with Modular Architecture

👉 Understanding Clean Architecture in iOS: A Beginner’s Guide

👉 MVC vs MVVM vs VIPER in iOS — Which Architecture Should You Choose in 2025?

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading