SOLID Principles in Swift: Theory and Practice for iOS Development

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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

Blogs

Book Recommendations