Introducción: Por qué SOLID es importante para un desarrollador Swift
Cada desarrollador iOS tarde o temprano se enfrenta al problema del "código espagueti": los controladores se hinchan hasta miles de líneas, cambiar una entidad rompe otras tres, y las pruebas se convierten en una pesadilla. Los principios SOLID son cinco reglas fundamentales del diseño orientado a objetos que ayudan a crear código flexible, mantenible y testeable en Swift.
En este artículo analizaremos cada principio SOLID con ejemplos concretos del desarrollo iOS. Aprenderás cómo aplicar estos principios en proyectos reales, evitando errores típicos. El material será útil tanto para desarrolladores Junior como para profesionales experimentados que deseen sistematizar sus conocimientos.
1. Single Responsibility Principle (SRP) — Principio de responsabilidad única
Esencia: Cada clase debe tener solo una razón para cambiar. En otras palabras, la clase debe ser responsable de una sola tarea.
Problema: El ViewController "divino"
Un error típico es poner toda la lógica en UIViewController. Veamos un mal ejemplo:
class UserProfileViewController: UIViewController { @IBOutlet weak var nameLabel: UILabel! func loadUser() { // 1. Lógica de red let url = URL(string: "https://api.example.com/user")! URLSession.shared.dataTask(with: url) { data, _, _ in // 2. Parseo de JSON let user = try! JSONDecoder().decode(User.self, from: data!) // 3. Guardado en Core Data self.saveToCoreData(user) // 4. Actualización de UI DispatchQueue.main.async { self.nameLabel.text = user.name } }.resume() } private func saveToCoreData(_ user: User) { /* ... */ }}Solución: Separación de responsabilidades
Dividamos el código en componentes separados:
// 1. Servicio para trabajar con la redclass 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. Gestor de almacenamiento de datosclass UserStorageManager { func save(_ user: User) { // Lógica de guardado en Core Data / Realm }}
// 3. ViewModel (o 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 ahora solo gestiona la 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() }}Resultado: Cada clase tiene una sola razón para cambiar. Probar y mantener este código es significativamente