Introduction to SOLID Principles in Swift
\nEvery iOS developer sooner or later faces the problem of "spaghetti code": classes bloat, changes in one place break the entire application, and testing becomes a nightmare. The solution to these problems lies at the core of object-oriented design — SOLID principles. These five rules, formulated by Robert Martin, help create flexible, maintainable, and scalable code in Swift. In this article, we will break down each principle from theory to practical examples for iOS/macOS.
\nWhether you are writing in UIKit, SwiftUI, or AppKit, SOLID is a universal foundation. Let's dive into the details.
\n\n1. Single Responsibility Principle (SRP)
\nTheory
\nA class should have only one reason to change. In other words, each module is responsible for exactly one functionality. If your ViewController manages the UI, handles network requests, and parses JSON — it violates SRP.
\nPractice in Swift
\nLet's consider a typical example — loading and displaying user data.
\n// ❌ Bad: SRP violation\nclass UserViewController: UIViewController {\n func loadUser() {\n // Network request\n let url = URL(string: "https://api.example.com/user")!\n URLSession.shared.dataTask(with: url) { data, _, _ in\n let user = try? JSONDecoder().decode(User.self, from: data!)\n DispatchQueue.main.async {\n self.updateUI(with: user)\n }\n }.resume()\n }\n \n func updateUI(with user: User?) {\n // Update UI\n }\n}\n// ✅ Good: separating responsibilities\nclass UserService {\n func fetchUser(completion: @escaping (User?) -> Void) {\n let url = URL(string: "https://api.example.com/user")!\n URLSession.shared.dataTask(with: url) { data, _, _ in\n let user = try? JSONDecoder().decode(User.self, from: data!)\n completion(user)\n }.resume()\n }\n}\n\nclass UserViewController: UIViewController {\n let userService = UserService()\n \n func loadUser() {\n userService.fetchUser { [weak self] user in\n DispatchQueue.main.async {\n self?.updateUI(with: user)\n }\n }\n }\n \n private func updateUI(with user: User?) {\n // Update UI\n }\n}\nConclusion: Now UserService is only responsible for networking, and UserViewController is only responsible for the UI. Changes in the network logic do not affect the controller.
2. Open-Closed Principle (OCP)
\nTheory
\nClasses should be open for extension but closed for modification. Instead of changing existing code, we add new functionality through inheritance or protocols. In Swift, protocols and extensions serve as the ideal tool for this.
\nPractice in Swift
\nImagine we are writing a module for calculating discounts in an online store.
\n// ❌ Bad: adding a new discount requires modifying the class\nclass DiscountCalculator {\n func calculate(price: Double, type: String) -> Double {\n if type == "seasonal" {\n return price * 0.9\n } else if type == "loyalty" {\n return price * 0.85\n }\n return price\n }\n}\n// ✅ Good: using a protocol for extension\nprotocol DiscountStrategy {\n func applyDiscount(to price: Double) -> Double\n}\n\nstruct SeasonalDiscount: DiscountStrategy {\n func applyDiscount(to price: Double) -> Double {\n price * 0.9\n }\n}\n\nstruct LoyaltyDiscount: DiscountStrategy {\n func applyDiscount(to price: Double) -> Double {\n price * 0.85\n }\n}\n\nclass DiscountCalculator {\n func calculate(price: Double, strategy: DiscountStrategy) -> Double {\n strategy.