Programación en Swift y SwiftUI para iOS Developers

Mejores patrones de diseño en SwiftUI

Introducción

El lanzamiento de SwiftUI marcó un antes y un después en la historia del desarrollo para plataformas Apple. Si llevas años trabajando como ios developer, es probable que tu mente esté cableada para pensar en MVC (Model-View-Controller) y en el ciclo de vida imperativo de UIKit. Sin embargo, al dar el salto a SwiftUI, muchos desarrolladores intentan forzar estos viejos patrones en el nuevo paradigma declarativo, resultando en código difícil de mantener y errores de estado impredecibles.

La programación Swift moderna requiere una nueva mentalidad. SwiftUI no es solo una nueva capa de pintura; es un cambio fundamental en cómo fluyen los datos. En este tutorial, desglosaremos los patrones de diseño más efectivos y “Swifty” para construir aplicaciones robustas en iOS, macOS y watchOS. Olvida el “Massive View Controller”; bienvenido a la era de la composición, el estado reactivo y la arquitectura limpia.


El Cambio de Paradigma: De Imperativo a Declarativo

Antes de sumergirnos en los patrones específicos, es crucial entender el terreno de juego. En UIKit, tú (el controlador) modificabas la vista. En SwiftUI, la vista es una función de su estado.

View=f(State)

Esto significa que no “cambias” una etiqueta de texto; cambias una variable de estado y SwiftUI redibuja la etiqueta por ti. Por lo tanto, los mejores patrones de diseño en SwiftUI son aquellos que gestionan el flujo de datos de manera eficiente, no los que gestionan la jerarquía de vistas directamente.


1. MVVM (Model-View-ViewModel): El Estándar de Oro

Aunque hay debates sobre si MVVM es estrictamente necesario en SwiftUI (dado que la propia vista ya maneja estado), sigue siendo el patrón dominante para separar la lógica de negocio de la interfaz de usuario.

¿Por qué MVVM en SwiftUI?

Como ios developer, quieres que tus vistas sean tontas (dumb views). Solo deben saber cómo pintar cosas, no cómo calcularlas o buscar datos en una API. El ViewModel actúa como el intermediario que transforma los datos del Modelo en algo que la Vista puede consumir.

Implementación Moderna con @Observable (Swift 5.9+)

Con las últimas actualizaciones de Swift y Xcode, ya no necesitamos conformar ObservableObject y usar @Published en todas partes. La macro @Observable ha simplificado este patrón drásticamente.

El Modelo:

struct User: Identifiable, Codable {
    let id: UUID
    let name: String
    let role: String
}

El ViewModel:

import Observation

@Observable
class UserListViewModel {
    var users: [User] = []
    var isLoading: Bool = false
    
    private let dataService: DataServiceProtocol // Inyección de dependencias
    
    init(dataService: DataServiceProtocol = DataService()) {
        self.dataService = dataService
    }
    
    func fetchUsers() async {
        isLoading = true
        do {
            self.users = try await dataService.getUsers()
        } catch {
            print("Error fetching users: \(error)")
        }
        isLoading = false
    }
}

La Vista (SwiftUI):

struct UserListView: View {
    // La vista es dueña del estado del ViewModel
    @State private var viewModel = UserListViewModel()
    
    var body: some View {
        NavigationStack {
            List(viewModel.users) { user in
                VStack(alignment: .leading) {
                    Text(user.name).font(.headline)
                    Text(user.role).font(.subheadline)
                }
            }
            .overlay {
                if viewModel.isLoading {
                    ProgressView()
                }
            }
            .task {
                await viewModel.fetchUsers()
            }
            .navigationTitle("Equipo iOS")
        }
    }
}

Ventaja Clave: La vista se actualiza automáticamente cuando users o isLoading cambian, sin necesidad de Combine explícito. Esto mantiene tu código de SwiftUI limpio y testear el ViewModel es trivial porque es una clase pura de Swift.


2. El Patrón Coordinator (Navigation Router)

Uno de los mayores dolores de cabeza en SwiftUI ha sido históricamente la navegación. Usar NavigationLink directamente dentro de las vistas crea un acoplamiento fuerte: la “Vista A” necesita saber que existe la “Vista B”. Esto rompe la modularidad.

Para aplicaciones profesionales en iOS y macOS, el patrón Coordinator (o Router) es esencial para desacoplar la lógica de navegación de la UI.

Implementación con NavigationStack

Usaremos un objeto observable para gestionar el path de navegación.

enum AppRoute: Hashable {
    case details(User)
    case settings
    case profile
}

@Observable
class NavigationRouter {
    var path = NavigationPath()
    
    func navigate(to route: AppRoute) {
        path.append(route)
    }
    
    func goBack() {
        path.removeLast()
    }
    
    func reset() {
        path = NavigationPath()
    }
}

Inyectando el Router en la raíz:

struct RootView: View {
    @State private var router = NavigationRouter()
    
    var body: some View {
        NavigationStack(path: $router.path) {
            HomeView()
                .environment(router) // Inyectamos al entorno
                .navigationDestination(for: AppRoute.self) { route in
                    switch route {
                    case .details(let user):
                        UserDetailsView(user: user)
                    case .settings:
                        SettingsView()
                    case .profile:
                        ProfileView()
                    }
                }
        }
    }
}

Ahora, cualquier vista secundaria puede acceder al router y navegar sin saber a qué vista está yendo realmente, solo sabe que va a una “ruta”. Esto es vital para un ios developer que busca escalabilidad.


3. Composition Pattern (View Composition)

En UIKit, a menudo teníamos ViewControllers gigantes. En SwiftUI, el rendimiento y la legibilidad dependen de dividir la UI en componentes pequeños y reutilizables. Este es el patrón de Composición.

No escribas una vista de 500 líneas. Si tienes una tarjeta de producto, extráela. Si tienes un botón personalizado, extráelo.

Anti-patrón (Evitar):

var body: some View {
    VStack {
        // 50 líneas de código para una imagen
        // 30 líneas para el texto
        // 20 modificadores...
    }
}

Patrón de Composición (Hacer):

struct ProductView: View {
    let product: Product
    
    var body: some View {
        VStack {
            ProductImageView(url: product.imageUrl)
            ProductInfoView(title: product.name, price: product.price)
            AddToCartButton(action: addToCart)
        }
        .padding()
        .cardStyle() // Modificador personalizado
    }
}

Este enfoque aprovecha el sistema de tipos de Swift y hace que las vistas previas en Xcode sean mucho más rápidas, ya que el compilador tiene menos trabajo que procesar en cada cambio.


4. The Environment Pattern (Inyección de Dependencias)

La inyección de dependencias (DI) es un pilar de la buena ingeniería de software. En SwiftUI, tenemos una herramienta nativa muy potente para esto: el Environment.

En lugar de pasar datos de padre a hijo, a nieto, a bisnieto (lo que se conoce como “prop drilling”), podemos inyectar dependencias en la jerarquía superior y leerlas donde sea necesario.

Definiendo una Clave de Entorno:

private struct AuthenticationKey: EnvironmentKey {
    static let defaultValue: AuthServiceProtocol = MockAuthService()
}

extension EnvironmentValues {
    var authService: AuthServiceProtocol {
        get { self[AuthenticationKey.self] }
        set { self[AuthenticationKey.self] = newValue }
    }
}

Uso:

// En el punto de entrada de la App
MyHighTechApp()
    .environment(\.authService, RealAuthService())

// En una vista profunda
struct LoginView: View {
    @Environment(\.authService) var authService
    
    func login() {
        authService.signIn()
    }
}

Este patrón es crucial para el testing. Un ios developer puede inyectar un servicio de red simulado (Mock) para las Previews de Xcode y el servicio real para la compilación final, sin cambiar una sola línea de código en la vista.


5. Factory Pattern (View Factory)

A veces, quieres que tu ViewModel decida qué mostrar, pero no quieres que el ViewModel importe SwiftUI (para mantenerlo puro). Aquí entra el patrón Factory.

Imagina una lista heterogénea donde cada elemento puede ser de un tipo diferente (Video, Imagen, Texto).

protocol ViewFactory {
    associatedtype ContentView: View
    func makeView(for item: FeedItem) -> ContentView
}

struct FeedViewFactory: ViewFactory {
    @ViewBuilder
    func makeView(for item: FeedItem) -> some View {
        switch item.type {
        case .video(let url):
            VideoPlayerView(url: url)
        case .image(let url):
            AsyncImage(url: url)
        case .text(let content):
            Text(content)
        }
    }
}

Tu vista principal simplemente itera sobre los items y le pide a la factoría que cree la vista correspondiente. Esto mantiene tu ForEach limpio y tu lógica de creación de vistas centralizada.


Optimización y Rendimiento: Lo que el Pro de Swift Debe Saber

Más allá de la arquitectura, aplicar bien los patrones implica entender cómo SwiftUI renderiza.

  1. Identidad de las Vistas: Usa id estables en tus listas. No uses UUID() generados en el momento, o forzarás redibujados innecesarios.
  2. @StateObject vs @ObservedObject: (Para versiones anteriores a iOS 17). Recuerda siempre que si la vista crea el modelo, usa @StateObject. Si la vista recibe el modelo, usa @ObservedObject. Confundir esto es la causa #1 de bugs donde los datos se reinician aleatoriamente.
  3. KeyPaths y Binding: Aprovecha la sintaxis de $ para crear bindings automáticos a tus ViewModels observables.

Conclusión: El Futuro es Declarativo

Dominar estos patrones de diseño te diferenciará del resto. Mientras que un principiante apila modificadores sin ton ni son, un ios developer experto estructura su aplicación utilizando MVVM para la lógica, Coordinators para la navegación y Composición para la UI.

La programación Swift está evolucionando. Adoptar estos patrones no solo hará que tu código sea más limpio y legible en Xcode, sino que también hará que tus aplicaciones para iOS, macOS y watchOS sean más robustas, escalables y fáciles de probar.

Tu siguiente paso: No intentes refactorizar toda tu app hoy. Empieza por extraer una vista grande usando el patrón de Composición, o implementa un NavigationRouter en tu próxima funcionalidad. La arquitectura es un camino, no un destino.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

SwiftUI: Componente DatePicker

Next Article

Cómo añadir una barra de búsqueda en SwiftUI

Related Posts