Introduction: Why SOLID is Important for a Swift Developer
Every iOS developer sooner or later faces the problem of "spaghetti code": controllers bloat to thousands of lines, changing one entity breaks three others, and testing becomes a nightmare. SOLID principles are five fundamental rules of object-oriented design that help create flexible, maintainable, and testable code in Swift.
In this article, we will break down each SOLID principle with specific examples from iOS development. You will learn how to apply these principles in real projects, avoiding common mistakes. The material will be useful for both Junior developers and experienced specialists looking to systematize their knowledge.
1. Single Responsibility Principle (SRP)
Essence: Each class should have only one reason to change. In other words, a class should be responsible for only one task.
Problem: The "God" ViewController
A typical mistake is putting all the logic into a UIViewController. Consider a bad example:
class UserProfileViewController: UIViewController { @IBOutlet weak var nameLabel: UILabel! func loadUser() { // 1. Network logic let url = URL(string: "https://api.example.com/user")! URLSession.shared.dataTask(with: url) { data, _, _ in // 2. JSON parsing let user = try! JSONDecoder().decode(User.self, from: data!) // 3. Saving to Core Data self.saveToCoreData(user) // 4. UI update DispatchQueue.main.async { self.nameLabel.text = user.name } }.resume() } private func saveToCoreData(_ user: User) { /* ... */ }}Solution: Separation of Responsibilities
Let's split the code into separate components:
// 1. Service for network operationsclass UserNetworkService { func fetchUser(completion: @escaping (Result<User, Error>) -> Void) { let url = URL(string: "https://api.example.com/user")! URLSession.shared.dataTask(with: url) { data, _, error in if let data = data { do { let user = try JSONDecoder().decode(User.self, from: data) completion(.success(user)) } catch { completion(.failure(error)) } } else if let error = error { completion(.failure(error)) } }.resume() }}
// 2. Data storage managerclass UserStorageManager { func save(_ user: User) { // Logic for saving to Core Data / Realm }}
// 3. ViewModel (or Presenter)class UserViewModel { private let networkService = UserNetworkService() private let storageManager = UserStorageManager() var onUserUpdated: ((User) -> Void)? func loadUser() { networkService.fetchUser { [weak self] result in switch result { case .success(let user): self?.storageManager.save(user) self?.onUserUpdated?(user) case .failure(let error): print(error.localizedDescription) } } }}
// 4. ViewController now only manages the UIclass UserProfileViewController: UIViewController { @IBOutlet weak var nameLabel: UILabel! private let viewModel = UserViewModel() override func viewDidLoad() { super.viewDidLoad() viewModel.onUserUpdated = { [weak self] user in DispatchQueue.main.async { self?.nameLabel.text = user.name } } viewModel.loadUser() }}Result: Each class has one reason to change. Testing and maintaining such code is significantly