Bird
Raised Fist0
iOS Swiftmobile~15 mins

Dependency injection in iOS Swift - Deep Dive

Choose your learning style10 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.

Practice

(1/5)
1. What is the main purpose of dependency injection in Swift programming?
easy
A. To provide objects with their dependencies from outside rather than creating them inside
B. To make the app run faster by caching data
C. To automatically generate user interfaces
D. To encrypt data before saving

Solution

  1. Step 1: Understand dependency injection concept

    Dependency injection means giving an object the things it needs (dependencies) from outside instead of creating them inside the object.
  2. Step 2: Identify the main purpose

    This approach helps keep code flexible, easier to test, and cleaner by separating concerns.
  3. Final Answer:

    To provide objects with their dependencies from outside rather than creating them inside -> Option A
  4. Quick Check:

    Dependency injection = provide dependencies from outside [OK]
Hint: Dependency injection means giving dependencies from outside [OK]
Common Mistakes:
  • Thinking it speeds up app performance
  • Confusing with UI generation
  • Mixing with data encryption
2. Which of the following is the correct syntax for constructor injection in Swift?
easy
A. class Client { func inject(service: Service) { self.service = service } }
B. class Service { init() { } } class Client { let service: Service; init(service: Service) { self.service = service } }
C. class Client { var service = Service() }
D. class Service { func start() {} }

Solution

  1. Step 1: Identify constructor injection pattern

    Constructor injection means passing dependencies through the initializer (init) of a class.
  2. Step 2: Check which code passes dependency via init

    class Service { init() { } } class Client { let service: Service; init(service: Service) { self.service = service } } shows a Client class receiving a Service instance through its init method and storing it.
  3. Final Answer:

    class Service { init() { } } class Client { let service: Service; init(service: Service) { self.service = service } } -> Option B
  4. Quick Check:

    Constructor injection uses init to pass dependencies [OK]
Hint: Constructor injection passes dependencies via init method [OK]
Common Mistakes:
  • Creating dependencies inside the class instead of passing them
  • Using methods instead of init for injection
  • Not storing the injected dependency
3. Given the code below, what will be printed?
protocol Logger { func log(_ message: String) }
class ConsoleLogger: Logger { func log(_ message: String) { print("Console: \(message)") } }
class Client {
  let logger: Logger
  init(logger: Logger) { self.logger = logger }
  func doWork() { logger.log("Work done") }
}
let client = Client(logger: ConsoleLogger())
client.doWork()
medium
A. No output
B. Work done
C. Console: Work done
D. Logger: Work done

Solution

  1. Step 1: Understand the Logger protocol and ConsoleLogger class

    ConsoleLogger implements Logger and prints messages prefixed with "Console:".
  2. Step 2: Trace the Client usage

    Client is initialized with ConsoleLogger and calls logger.log("Work done"), which prints "Console: Work done".
  3. Final Answer:

    Console: Work done -> Option C
  4. Quick Check:

    Injected ConsoleLogger prints with "Console:" prefix [OK]
Hint: Injected logger prints message with prefix [OK]
Common Mistakes:
  • Ignoring the prefix in print statement
  • Assuming no output because of protocol
  • Confusing method names
4. Identify the error in the following Swift code using dependency injection:
protocol DataService { func fetchData() -> String }
class RealDataService: DataService { func fetchData() -> String { "Real Data" } }
class ViewModel {
  var dataService: DataService
  init() { }
  func load() -> String { dataService.fetchData() }
}
medium
A. dataService is not initialized before use
B. fetchData() should return Int, not String
C. ViewModel should not have an init method
D. RealDataService does not conform to DataService

Solution

  1. Step 1: Check property initialization

    ViewModel has a non-optional property dataService but its init() does not initialize it.
  2. Step 2: Understand Swift initialization rules

    Non-optional properties must be initialized before use; missing initialization causes a compile error.
  3. Final Answer:

    dataService is not initialized before use -> Option A
  4. Quick Check:

    Non-optional properties must be initialized [OK]
Hint: Non-optional properties must be set in init [OK]
Common Mistakes:
  • Assuming default nil for non-optional
  • Confusing return types
  • Ignoring protocol conformance
5. You want to make a Swift class NetworkManager testable by injecting a protocol NetworkService. Which approach best applies dependency injection and allows easy swapping of real and mock services?
hard
A. Create NetworkManager with a fixed real network service inside the class
B. Make NetworkManager inherit from NetworkService and override methods
C. Use global variables to hold the network service instance
D. Define a NetworkService protocol, make NetworkManager accept it via init, and pass either real or mock implementations when creating NetworkManager

Solution

  1. Step 1: Understand testability with dependency injection

    To test NetworkManager easily, inject a protocol so you can swap real and mock implementations.
  2. Step 2: Identify best practice

    Passing NetworkService via init allows NetworkManager to use any implementation, making testing flexible.
  3. Final Answer:

    Define a NetworkService protocol, make NetworkManager accept it via init, and pass either real or mock implementations when creating NetworkManager -> Option D
  4. Quick Check:

    Inject protocol via init for flexible testing [OK]
Hint: Inject protocol via init to swap real/mock easily [OK]
Common Mistakes:
  • Hardcoding real service inside class
  • Using global variables causing tight coupling
  • Misusing inheritance instead of composition
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard