Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
248: Dependency Injection Patterns in Swift
I've spent way too many hours of my career fighting with "untestable" code. Usually, the culprit is the same: a class that decides exactly which other classes it wants to work with, hardcoding those dependencies right into its own initializer. It feels efficient at first, but the moment you try to write a unit test without triggering a real network call or writing to a production database, you'll hit a wall.
Let's look at how to fix this using Dependency Injection (DI) patterns in Swift. We're going to build a simple WeatherViewModel that fetches the current temperature.
The mistake of hardcoding dependencies
If I were rushing a feature, I might write something like this. I'll just create the service inside the view model and call it a day:
class WeatherService {
func fetchTemperature() -> String {
// Imagine a real URLSession call here
return "72°F"
}
}
class WeatherViewModel {
private let service = WeatherService()
func getWeather() -> String {
return service.fetchTemperature()
}
}
Now, here is where I've messed up. If I want to test WeatherViewModel, I'm forced to use the real WeatherService. If the API is down or the internet is flaky, my tests fail. The WeatherViewModel is "tightly coupled" to the concrete WeatherService class. It's stuck.
Breaking the tie with a Protocol
To fix this, we need to stop the view model from caring which class is doing the work, as long as that class knows how to fetch a temperature. In Swift, the best way to do this is with a protocol. I'm going to define the "behavior" separately from the "implementation."
protocol WeatherProviding {
func fetchTemperature() -> String
}
class RealWeatherService: WeatherProviding {
func fetchTemperature() -> String {
return "72°F" // Real API logic goes here
}
}
By doing this, I've created a contract. Anything that conforms to WeatherProviding can now be used by our view model.
Injecting the dependency via the initializer
Now for the actual "Injection" part. Instead of the view model creating its own service, I'm going to tell the view model: "You don't make the service; I'll give you one when I create you." This is called Constructor Injection, and it's the most robust DI pattern in Swift.
class WeatherViewModel {
private let service: WeatherProviding
// We inject the dependency here
init(service: WeatherProviding) {
self.service = service
}
func getWeather() -> String {
return service.fetchTemperature()
}
}
Notice that WeatherViewModel no longer knows RealWeatherService exists. It only knows about the protocol. This is a huge win for flexibility.
Testing with a Mock service
The real magic happens when we write our tests. I can now create a "Mock" version of the service that returns a hardcoded value instantly, without ever touching the network. This makes tests fast and deterministic.
class MockWeatherService: WeatherProviding {
var mockTemperature = "0°F"
func fetchTemperature() -> String {
return mockTemperature
}
}
// In our test file:
let mock = MockWeatherService()
mock.mockTemperature = "32°F"
let viewModel = WeatherViewModel(service: mock)
print(viewModel.getWeather()) // Always returns "32°F" regardless of the weather outside
I've now completely decoupled the business logic of the view model from the data-fetching logic of the service. If we ever decide to switch from a REST API to GraphQL or a local database, we just create a new class conforming to WeatherProviding and swap it out at the top level of the app. The view model doesn't change a single line of code.
📋 Practical Task
Exercise: Decoupling a UserProfileViewModel
You have a UserProfileViewModel that is currently tightly coupled to a DatabaseManager. Your task is to refactor this code to use Constructor Injection so that it can be tested with a mock database.
Current Code:
class DatabaseManager {
func getUserName(id: Int) -> String {
return "Real User from DB"
}
}
class UserProfileViewModel {
private let db = DatabaseManager()
func displayName(for id: Int) -> String {
return db.getUserName(id: id)
}
}
Your Requirements:
- Create a protocol named
UserProvidingthat defines thegetUserName(id:)method. - Make
DatabaseManagerconform toUserProviding. - Modify
UserProfileViewModelto accept anyUserProvidingobject through its initializer. - Create a
MockUserDatabaseclass that conforms toUserProvidingand returns the string "Mock User". - Instantiate the
UserProfileViewModelusing theMockUserDatabase.
There are no comments for now.