0
0
iOS Swiftmobile~15 mins

Dependency injection in iOS Swift - Deep Dive

Choose your learning style9 modes available
Overview - Dependency injection
What is it?
Dependency injection is a way to give an object the things it needs to work, instead of the object creating them itself. It helps separate the parts of your app so they can work independently. This makes your code easier to test, change, and understand.
Why it matters
Without dependency injection, objects tightly depend on each other, making apps hard to fix or improve. If one part changes, many others might break. Dependency injection solves this by clearly defining what each part needs, so you can swap or test parts easily without breaking the whole app.
Where it fits
Before learning dependency injection, you should understand basic Swift classes and how objects work. After this, you can learn about design patterns like MVVM or Coordinator, which often use dependency injection to organize app structure.
Mental Model
Core Idea
Dependency injection means giving an object its needed parts from outside, so it doesn’t have to create or find them itself.
Think of it like...
Imagine you’re building a toy car. Instead of making the wheels yourself, someone hands you the wheels to attach. You just focus on assembling the car, not making every piece.
┌───────────────┐       inject       ┌───────────────┐
│   Car Object  │  <───────────────  │  Wheels Object │
└───────────────┘                   └───────────────┘

Car doesn’t build wheels; wheels are given to car.
Build-Up - 7 Steps
1
FoundationWhat is dependency injection
🤔
Concept: Introducing the idea that objects can receive their dependencies from outside instead of creating them.
In Swift, a class might need another class to work. Instead of creating that inside, we pass it in. For example, a ViewController needs a DataService. Instead of making DataService inside, we give it from outside when creating ViewController.
Result
Objects become simpler and only focus on their job, not on creating other objects.
Understanding that objects don’t have to create everything themselves helps keep code clean and flexible.
2
FoundationTypes of dependency injection
🤔
Concept: Learning the three main ways to inject dependencies: initializer, property, and method injection.
1. Initializer injection: pass dependencies when creating the object. 2. Property injection: set dependencies after creating the object. 3. Method injection: pass dependencies when calling a method.
Result
You can choose how and when to provide dependencies based on your app’s needs.
Knowing different injection types helps you pick the best way to supply dependencies in different situations.
3
IntermediateUsing initializer injection in Swift
🤔Before reading on: do you think initializer injection makes dependencies optional or required? Commit to your answer.
Concept: Initializer injection requires dependencies when creating an object, making them mandatory and clear.
class DataService {} class ViewController { let dataService: DataService init(dataService: DataService) { self.dataService = dataService } } // Usage let service = DataService() let vc = ViewController(dataService: service)
Result
ViewController always has a DataService when created, avoiding missing dependencies.
Understanding that initializer injection enforces required dependencies prevents runtime errors from missing parts.
4
IntermediateProperty injection and its use cases
🤔Before reading on: do you think property injection is safer or riskier than initializer injection? Commit to your answer.
Concept: Property injection sets dependencies after object creation, allowing optional or changeable dependencies.
class ViewController { var dataService: DataService? } let vc = ViewController() vc.dataService = DataService()
Result
You can create objects without dependencies and add them later, but risk forgetting to set them.
Knowing property injection allows flexibility but requires care to avoid missing dependencies at runtime.
5
IntermediateMethod injection for temporary dependencies
🤔
Concept: Passing dependencies only when calling a method, useful for short-lived or one-time needs.
class ViewController { func fetchData(using service: DataService) { // use service here } } let vc = ViewController() vc.fetchData(using: DataService())
Result
Dependencies are only needed during method calls, keeping objects lighter.
Understanding method injection helps manage dependencies that are not needed all the time.
6
AdvancedDependency injection with protocols for flexibility
🤔Before reading on: do you think using protocols makes dependency injection more or less flexible? Commit to your answer.
Concept: Using protocols lets you inject any object that follows the protocol, not just one concrete class.
protocol DataServiceProtocol { func fetch() } class RealDataService: DataServiceProtocol { func fetch() { /* real fetch */ } } class MockDataService: DataServiceProtocol { func fetch() { /* mock fetch */ } } class ViewController { let dataService: DataServiceProtocol init(dataService: DataServiceProtocol) { self.dataService = dataService } }
Result
You can swap real or mock services easily, improving testing and flexibility.
Knowing protocols decouple code from concrete implementations, making apps easier to maintain and test.
7
ExpertUsing dependency injection containers in Swift
🤔Before reading on: do you think containers simplify or complicate dependency management? Commit to your answer.
Concept: Containers automatically create and provide dependencies, managing object lifecycles and reducing manual wiring.
class Container { private var services = [String: Any]() func register(_ service: T) { let key = String(describing: T.self) services[key] = service } func resolve() -> T? { let key = String(describing: T.self) return services[key] as? T } } // Usage let container = Container() container.register(DataService()) let service: DataService? = container.resolve()
Result
Dependencies are managed centrally, reducing boilerplate and improving scalability.
Understanding containers helps manage complex apps with many dependencies efficiently.
Under the Hood
Dependency injection works by passing references to objects instead of creating them inside. At runtime, the injector provides the needed objects, so the dependent object only holds references. This reduces tight coupling and allows swapping implementations without changing dependent code.
Why designed this way?
It was designed to solve the problem of tightly coupled code that is hard to test and maintain. By separating creation from use, developers can write modular, reusable, and testable code. Alternatives like global singletons were rejected because they hide dependencies and cause hidden side effects.
┌───────────────┐       ┌───────────────┐       ┌───────────────┐
│  Injector     │──────▶│  Dependency   │──────▶│  Dependent    │
│ (creates and  │       │  (service)    │       │  (client)     │
│  provides)    │       └───────────────┘       └───────────────┘
└───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does dependency injection mean creating new objects inside the dependent class? Commit yes or no.
Common Belief:Dependency injection means the object creates its own dependencies internally.
Tap to reveal reality
Reality:Dependency injection means the object receives dependencies from outside, not creating them itself.
Why it matters:If you create dependencies inside, you lose flexibility and testability, making code harder to maintain.
Quick: Is dependency injection only useful for testing? Commit yes or no.
Common Belief:Dependency injection is just a testing trick to swap mocks.
Tap to reveal reality
Reality:Dependency injection improves code design, flexibility, and maintainability beyond testing.
Why it matters:Limiting DI to testing misses its full benefits in building clean, modular apps.
Quick: Does using dependency injection containers always make code simpler? Commit yes or no.
Common Belief:Dependency injection containers always simplify dependency management.
Tap to reveal reality
Reality:Containers add complexity and are best for large apps; small apps may be simpler without them.
Why it matters:Using containers unnecessarily can overcomplicate simple projects and confuse beginners.
Quick: Can you inject dependencies without protocols? Commit yes or no.
Common Belief:Protocols are not needed for dependency injection; concrete classes are enough.
Tap to reveal reality
Reality:Protocols enable flexible swapping of implementations, making DI more powerful and testable.
Why it matters:Skipping protocols leads to tight coupling and harder-to-test code.
Expert Zone
1
Dependency injection containers often support scopes like singleton or transient, controlling object lifetimes precisely.
2
Lazy injection delays creating dependencies until they are needed, improving app startup performance.
3
Combining dependency injection with Swift property wrappers can simplify injecting dependencies with less boilerplate.
When NOT to use
Avoid dependency injection in very small or simple apps where it adds unnecessary complexity. Instead, create objects directly. Also, avoid overusing containers in apps with few dependencies to keep code straightforward.
Production Patterns
In real apps, dependency injection is combined with protocols and containers to manage complex object graphs. It enables easy testing by swapping real services with mocks. Patterns like MVVM or Coordinator rely heavily on DI to keep components independent and testable.
Connections
Inversion of Control (IoC)
Dependency injection is a form of IoC where control of creating dependencies is inverted from the object to an external source.
Understanding IoC helps grasp why dependency injection improves modularity by shifting responsibility for object creation.
Factory Pattern
Factories create objects and can be used alongside dependency injection to supply dependencies dynamically.
Knowing factories helps understand how dependencies can be created flexibly before injection.
Supply Chain Management
Both manage providing needed parts to a final product efficiently and on time.
Seeing dependency injection like supply chain management clarifies the importance of delivering the right parts to the right place without delays.
Common Pitfalls
#1Forgetting to inject a required dependency causes runtime crashes.
Wrong approach:class ViewController { var dataService: DataService? } let vc = ViewController() // Forgot to set dataService vc.dataService?.fetch() // crashes because dataService is nil
Correct approach:class ViewController { let dataService: DataService init(dataService: DataService) { self.dataService = dataService } } let service = DataService() let vc = ViewController(dataService: service) vc.dataService.fetch() // safe call
Root cause:Using optional property injection without ensuring the dependency is set leads to nil errors.
#2Tightly coupling code by injecting concrete classes instead of protocols.
Wrong approach:class ViewController { let dataService: DataService init(dataService: DataService) { self.dataService = dataService } }
Correct approach:protocol DataServiceProtocol {} class ViewController { let dataService: DataServiceProtocol init(dataService: DataServiceProtocol) { self.dataService = dataService } }
Root cause:Not using protocols reduces flexibility and testability.
#3Overusing dependency injection containers in small projects adds unnecessary complexity.
Wrong approach:let container = Container() container.register(DataService()) // For a tiny app with only one dependency
Correct approach:let service = DataService() let vc = ViewController(dataService: service) // Simple direct injection without container
Root cause:Misunderstanding when containers add value leads to over-engineering.
Key Takeaways
Dependency injection means giving objects what they need from outside, not making them create those parts themselves.
Using initializer injection makes dependencies required and clear, reducing runtime errors.
Protocols combined with dependency injection allow flexible swapping of implementations, improving testing and maintenance.
Dependency injection containers help manage complex dependencies but can overcomplicate small projects.
Avoid common mistakes like forgetting to inject dependencies or tightly coupling code to concrete classes.