La transición de UIKit a SwiftUI ha sido uno de los cambios más sísmicos en la historia del desarrollo para plataformas Apple. Como iOS Developer, es probable que hayas pasado años perfeccionando el patrón MVC (Model-View-Controller) en Xcode. Sin embargo, al abrir un proyecto nuevo en SwiftUI, te das cuenta de que las viejas reglas ya no aplican de la misma manera.
La naturaleza declarativa de la programación Swift moderna requiere una nueva mentalidad. Los controladores de vista han desaparecido, el estado es la única fuente de la verdad y la interfaz de usuario es una función directa de ese estado.
En este tutorial exhaustivo, exploraremos los patrones de diseño en SwiftUI más efectivos. Aprenderás a estructurar aplicaciones robustas, escalables y testables para iOS, macOS y watchOS, elevando tu nivel de código de junior a arquitecto de software.
¿Por qué los Patrones de Diseño siguen importando en SwiftUI?
Podrías pensar que SwiftUI, con su sintaxis concisa y su “magia” de enlace de datos (binding), elimina la necesidad de patrones complejos. Nada más lejos de la realidad. La facilidad para crear vistas en SwiftUI es un arma de doble filo. Es muy fácil caer en la trampa de escribir “Massive Views”, donde la lógica de negocio y el diseño visual se mezclan en un solo archivo.
Utilizar patrones de diseño correctos te permite:
- Separación de Responsabilidades: Mantener la lógica fuera de la UI.
- Testabilidad: Escribir pruebas unitarias en Xcode sin depender del simulador.
- Reusabilidad: Compartir código entre iOS, macOS y watchOS.
- Mantenibilidad: Hacer que tu código sea legible para otros desarrolladores Swift.
1. MVVM (Model-View-ViewModel): El Estándar de Oro
Si MVC era el rey de UIKit, MVVM es el emperador de SwiftUI. Este patrón se adapta de forma natural a la arquitectura reactiva de Apple.
¿Cómo funciona en el ecosistema Apple?
- Model (Modelo): Tus datos puros. Structs simples en Swift que no saben nada de la interfaz.
- View (Vista): La estructura visual en SwiftUI. Es declarativa y reacciona a los cambios.
- ViewModel (Vista-Modelo): El intermediario. Transforma los datos del modelo en algo que la vista pueda mostrar y maneja la lógica de negocio.
Implementación Práctica en Xcode
Vamos a crear una pantalla que muestra el estado de un servidor.
El Modelo:
struct ServerStatus: Identifiable {
let id = UUID()
let name: String
let isOnline: Bool
}
El ViewModel:
Aquí es donde brilla la programación Swift. Usamos el protocolo ObservableObject y el wrapper @Published para notificar cambios a la vista.
import Foundation
// MainActor asegura que las actualizaciones de UI ocurran en el hilo principal
@MainActor
class ServerListViewModel: ObservableObject {
@Published var servers: [ServerStatus] = []
@Published var isLoading: Bool = false
func fetchServers() async {
isLoading = true
// Simulación de red (espera de 1 segundo)
try? await Task.sleep(nanoseconds: 1_000_000_000)
self.servers = [
ServerStatus(name: "Servidor Alpha", isOnline: true),
ServerStatus(name: "Servidor Beta", isOnline: false),
ServerStatus(name: "Base de Datos", isOnline: true)
]
isLoading = false
}
}
La Vista (SwiftUI):
La vista “observa” al ViewModel. Cuando servers o isLoading cambian, la vista se redibuja automáticamente.
import SwiftUI
struct ServerListView: View {
// StateObject mantiene vivo el ViewModel mientras la vista exista
@StateObject private var viewModel = ServerListViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
ProgressView("Conectando...")
} else {
List(viewModel.servers) { server in
HStack {
Text(server.name)
Spacer()
Image(systemName: server.isOnline ? "checkmark.circle.fill" : "xmark.circle.fill")
.foregroundColor(server.isOnline ? .green : .red)
}
}
}
}
.navigationTitle("Estado del Sistema")
.task {
await viewModel.fetchServers()
}
}
}
}
Nota para el iOS Developer: Usa @StateObject cuando la vista crea el ViewModel por primera vez, y @ObservedObject cuando el ViewModel se pasa como dependencia desde otra vista.
2. El Patrón Observer (Observador)
Aunque MVVM lo utiliza implícitamente, entender el patrón Observer es crucial. SwiftUI se basa completamente en este patrón: cuando los datos cambian, la interfaz se actualiza.
Uso Moderno con el Framework Observation (Swift 5.9+)
Si estás utilizando Xcode 15 o superior y apuntas a iOS 17, el patrón Observer se ha simplificado drásticamente con la macro @Observable, eliminando la necesidad de Combine en muchos casos.
import Observation
@Observable
class UserSettings {
var username: String = "Invitado"
var isDarkMode: Bool = false
}
struct SettingsView: View {
// No necesitamos @StateObject ni @ObservedObject, solo var/let con @State
@State var settings = UserSettings()
var body: some View {
Form {
TextField("Nombre", text: $settings.username)
Toggle("Modo Oscuro", isOn: $settings.isDarkMode)
}
.onChange(of: settings.isDarkMode) { oldValue, newValue in
print("El usuario cambió el tema a: \(newValue)")
}
}
}
3. Patrón de Composición (Composition Pattern)
Uno de los errores más comunes al aprender patrones de diseño en SwiftUI es tratar de meter todo en una sola vista. SwiftUI prefiere vistas pequeñas y compuestas. La composición no es solo “dividir código”, es una estrategia arquitectónica.
Container Views (Vistas Contenedoras)
Imagina que quieres crear una tarjeta de estilo consistente en toda tu app. En lugar de copiar y pegar modificadores, creas una vista contenedora genérica.
struct CardView<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
.padding()
.background(Color(.systemBackground))
.cornerRadius(16)
.shadow(radius: 4)
.padding(.horizontal)
}
}
Ahora cualquier iOS Developer en tu equipo puede usar este componente sin saber cómo está diseñado internamente:
CardView {
VStack(alignment: .leading) {
Text("SwiftUI es increíble")
.font(.headline)
Text("La composición hace el código más limpio.")
.font(.caption)
}
}
4. Patrón ViewModifier (Decorador)
Relacionado con la composición, el patrón Decorator en SwiftUI se implementa a través de ViewModifier. Te permite encapsular estilos o comportamientos complejos y aplicarlos a cualquier vista.
struct CTAStyle: ViewModifier {
func body(content: Content) -> some View {
content
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(LinearGradient(colors: [.blue, .purple], startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.shadow(radius: 5)
}
}
// Extensión para facilitar el uso en Swift
extension View {
func ctaStyle() -> some View {
modifier(CTAStyle())
}
}
// Uso
// Button("Acción").ctaStyle()
5. El Patrón Coordinator (Coordinador) y la Navegación
La navegación ha sido históricamente un punto complejo. En UIKit usábamos Coordinators para desacoplar la lógica de navegación de los controladores. En las versiones modernas de SwiftUI (iOS 16+), el patrón Coordinator renace gracias a NavigationStack.
El Router / Coordinator
enum Route: Hashable {
case profile(userID: String)
case settings
case detail(item: String)
}
class NavigationCoordinator: ObservableObject {
@Published var path = NavigationPath()
func navigate(to route: Route) {
path.append(route)
}
func goBack() {
if !path.isEmpty { path.removeLast() }
}
func popToRoot() {
path = NavigationPath()
}
}
Inyección en la Vista Raíz
Con este patrón, la vista no sabe a dónde va, solo le dice al coordinador “quiero ir al perfil”.
struct ContentView: View {
@StateObject private var coordinator = NavigationCoordinator()
var body: some View {
NavigationStack(path: $coordinator.path) {
VStack {
Button("Ir al Perfil") {
coordinator.navigate(to: .profile(userID: "123"))
}
}
.navigationDestination(for: Route.self) { route in
switch route {
case .profile(let id):
Text("Perfil del usuario \(id)")
case .settings:
Text("Configuración")
case .detail(let item):
Text("Detalle: \(item)")
}
}
}
.environmentObject(coordinator)
}
}
6. Factory Pattern (Fábrica)
En aplicaciones complejas, a veces necesitas crear vistas basadas en lógica dinámica o configuraciones que vienen del servidor. El patrón Factory ayuda a encapsular esta creación, manteniendo tu código limpio en Xcode.
struct FeedItem {
enum ItemType { case video, image, text }
let type: ItemType
let content: String
}
struct FeedViewFactory {
@ViewBuilder
static func makeView(for item: FeedItem) -> some View {
switch item.type {
case .video:
Text("Reproductor de Video: \(item.content)") // Simplificado
case .image:
AsyncImage(url: URL(string: item.content))
case .text:
Text(item.content).font(.body)
}
}
}
Dentro de tu lista en SwiftUI, delegas la creación a la fábrica:
List(items, id: \.content) { item in
FeedViewFactory.makeView(for: item)
}
7. Inyección de Dependencias (Dependency Injection)
Para que tus ViewModels sean testables, no deben instanciar sus servicios directamente. Este es un pilar fundamental en la programación Swift avanzada.
// MALA PRÁCTICA: Acoplamiento fuerte
class LoginViewModel: ObservableObject {
let service = AuthService()
}
// BUENA PRÁCTICA: Inyección de Dependencias
class LoginViewModel: ObservableObject {
let service: AuthServiceProtocol
init(service: AuthServiceProtocol) {
self.service = service
}
}
Esto permite que, al ejecutar tests, inyectes un MockAuthService en lugar del real, haciendo tus pruebas instantáneas y fiables.
Conclusión: Eligiendo el patrón correcto
Como hemos visto, la programación Swift y SwiftUI no eliminan la arquitectura; la hacen más explícita. Dominar estos patrones de diseño en SwiftUI te transformará de alguien que “escribe código” a un verdadero ingeniero de software capaz de crear soluciones robustas en iOS, macOS y watchOS.
Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram










