SOLID Principles in Swift: Theory and Practice from A to Z

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 to SOLID Principles in Swift

\n

Every 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.

\n

Whether you are writing in UIKit, SwiftUI, or AppKit, SOLID is a universal foundation. Let's dive into the details.

\n\n

1. Single Responsibility Principle (SRP)

\n

Theory

\n

A 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.

\n

Practice in Swift

\n

Let'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}
\n

Conclusion: 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.

\n\n

2. Open-Closed Principle (OCP)

\n

Theory

\n

Classes 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.

\n

Practice in Swift

\n

Imagine 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.                

Blogs

Book Recommendations