iOS

Diseño de sistemas

Guía de Arquitectura de iOS

Un recorrido por cómo se diseñan las apps de iOS a escala — patrones, límites de módulos, flujo de datos, concurrencia y lanzamiento. Este es el material de "cómo construiría una app iOS grande" que diferencia las respuestas de senior y arquitecto del trabajo de funcionalidades.

01 · Patrones de arquitectura: MVC → MVVM → TCA

iOS ha pasado del MVC de UIKit (que a menudo se convertía en "Massive View Controller") a MVVM, donde un view model mantiene la lógica de presentación y el estado, y la vista permanece delgada y testeable. El flujo de datos de SwiftUI es naturalmente compatible con MVVM; algunos equipos van más lejos con un enfoque unidireccional / TCA (The Composable Architecture): estado en un lugar, cambios vía acciones a través de un reducer, efectos secundarios aislados y testeables.

Cómo elegir App pequeña/mediana o equipo nuevo en esto: MVVM con el framework de Observación. App grande, muchos ingenieros, mucha lógica, un premium en testeabilidad y consistencia: una arquitectura unidireccional (TCA o un equivalente hecho a mano) rinde frutos. "Emparejo la arquitectura con el equipo y la complejidad del producto — no copio un mismo patrón en todas partes."

02 · Modularización con paquetes Swift

A medida que una app crece, un solo target se convierte en un cuello de botella: compilaciones lentas, dependencias enredadas y dolor de merge. Divídalo en paquetes Swift locales — módulos de funcionalidad más paquetes compartidos Core / DesignSystem / Networking — con una dirección de dependencia explícita (las funcionalidades dependen del core, nunca entre sí).

Resultado Compilaciones incrementales y paralelas más rápidas, límites forzados (sin importaciones accidentales entre funcionalidades), pruebas y previews a nivel de funcionalidad, y la posibilidad de construir una funcionalidad de forma aislada. Esta es la decisión de arquitectura de mayor impacto en un código iOS grande.

03 · Inyección de dependencias y testeabilidad

Dependa de protocolos, no de tipos concretos, y pase las dependencias (inyección por initializer o @Environment de SwiftUI) en lugar de recurrir a singletons. Esa única disciplina hace que los view models sean testeables con fakes, desacopla las funcionalidades de la infraestructura y le permite intercambiar implementaciones (por ejemplo, un stub API en previews).

Señal de alerta Un view model que construye URLSession.shared o un singleton global internamente no puede probarse sin la red. "Inyecto un protocolo APIClient para que el view model nunca hable con la red directamente — eso es lo que lo hace testeable con un fake."

04 · Red y patrón repository

Ponga un repository entre su UI y las capas de red/persistencia. Los view models piden al repository los modelos de dominio; el repository posee el APIClient, descodificación, caché y la decisión de cuándo servir datos en caché vs frescos. Esto mantiene una única fuente de verdad y un único lugar para agregar reintentos, renovación de auth y paginación.

Argumento "UI → ViewModel → Repository → (APIClient + Store)." Cada capa tiene un trabajo y un seam de protocolo para pruebas. La caché y el comportamiento sin conexión viven en el repository, no esparcidos por las vistas.

05 · Offline-first y sincronización

Para apps que deben funcionar sin conexión, haga del almacenamiento local la fuente de verdad: la UI siempre lee de SwiftData/Core Data, y un motor de sincronización reconcilia con el servidor en segundo plano. Necesita una estrategia para la resolución de conflictos (último escritor gana, servidor autoritativo o fusión por campo), rastreo de cambios y reintento de mutaciones fallidas.

Partes difíciles Orden e idempotencia de mutaciones encoladas, desviación de reloj y fallos parciales. "Diseño la cola de mutaciones y la política de conflictos de forma explícita desde el principio — la sincronización offline falla en los bordes, no en el camino feliz."

06 · Arquitectura de navegación

Centralice el enrutamiento para que la navegación sean datos, no NavigationLinks esparcidos. Un router/coordinator posee una ruta NavigationStack de valores Route; las funcionalidades emiten rutas, el router decide cómo presentarlas. Los deep links y notificaciones push se descodifican en los mismos valores Route, y la restauración de estado se convierte en "persistir y recargar la ruta".

Por qué Desacoplar "qué mostrar" de "cómo presentarlo" mantiene las funcionalidades independientes, hace que el deep linking y los flujos A/B-tested sean triviales, y le da un lugar para razonar sobre todo el grafo de navegación. "La navegación es datos en mis apps — un router posee la ruta, así que los deep links, las notificaciones push y la restauración de estado solo agregan una Route."

07 · Arquitectura de concurrencia

Diseñe el aislamiento deliberadamente: la UI y los view models se ejecutan en el @MainActor; el estado mutable compartido (cachés, tiendas en memoria) vive detrás de actors; el trabajo costoso se ejecuta en tareas de fondo y vuelve al actor principal solo para publicar resultados. Bajo Swift 6, hacer los tipos Sendable y respetar el aislamiento se aplica en tiempo de compilación — por lo que la arquitectura debe ser intencional, no accidental.

Señal de alerta Esparcir DispatchQueue.main.async y locks por todas partes es síntoma de un aislamiento no diseñado. "Pongo el estado mutable compartido detrás de actors y fijo la UI a @MainActor para que el compilador demuestre que estoy libre de carreras, en vez de vigilarlo por convención."

08 · Observabilidad, banderas y lanzamiento

La preparación para producción es parte de la arquitectura. Conecte logging estructurado (OSLog), métricas/MetricKit y reporte de crashes; proteja funcionalidades riesgosas detrás de feature flags para que pueda distribuir a oscuras e implementar gradualmente; y use phased release en App Store con un plan de rollback (una bandera para desactivar, o una corrección expedita). Defina presupuestos — lanzamiento en frío, tasa sin crashes — y monitoreelos.

Perspectiva de Arquitecto "¿Cómo sabe que está saludable, y cómo lo apaga si no lo está?" Una respuesta segura a esa pregunta — banderas, paneles, phased rollout, rollback — es lo que diferencia el pensamiento a nivel de arquitecto.

Inmersiones

El playbook de senior en la forma de concepto → ejemplo → problema → solución, para que cada idea se fije como una decisión de ingeniería real en lugar de una definición.

Estado

MVVM vs The Composable Architecture

Concepto MVVM mantiene el estado en view models; TCA centraliza el estado y enruta cada cambio a través de acciones y un reducer, aislando efectos secundarios.
Ejemplo Un checkout de múltiples pasos con estado compartido, analíticas y validación compleja entre pantallas.
Problema Con MVVM ad-hoc, el estado se filtra entre view models, los bugs de orden se cuelan y los efectos secundarios son difíciles de probar.
Solución Un store unidireccional hace que las transiciones de estado sean explícitas y testeables de forma exhaustiva; recurra a ella cuando la complejidad y el tamaño del equipo justifiquen la ceremonia — de lo contrario MVVM es más ligero.
Modularidad

El monolito modular con SPM

Concepto Mantenga una app, pero divídala en paquetes Swift locales con una dirección de dependencia estricta.
Ejemplo 30 ingenieros, un target de app, compilaciones incrementales de 12 minutos y constantes conflictos de merge.
Problema Todo depende de todo; un cambio en cualquier lugar recompila el mundo y riesga romper funcionalidades no relacionadas.
Solución Los paquetes de funcionalidad dependen solo de Core / DesignSystem; las compilaciones se paralelizan, los límites son forzados por el compilador, y las funcionalidades se distribuyen y prueban independientemente. "Dejo que el compilador imponga los límites de módulo en lugar de una regla de lint o una convención de code review."
Datos

Sincronización offline-first

Concepto La base de datos local es la fuente de verdad; un motor de sincronización reconcilia con el servidor en segundo plano.
Ejemplo Una app de notas que los usuarios editan en el metro sin señal.
Problema Un "guardar en el servidor al tocar" ingenuo pierde ediciones sin conexión y muestra spinners por todas partes.
Solución Escriba localmente y renderice instantáneamente; encole mutaciones con claves de idempotencia; sincronice con una política de conflictos definida y reintente al reconectar. "El almacenamiento local es la fuente de verdad — la UI nunca espera a la red para mostrar lo que el usuario acaba de hacer."
Concurrencia

Estado compartido basado en actors

Concepto Un actor serializa el acceso a estado mutable, eliminando carreras de datos por construcción.
Ejemplo Una caché de imágenes en memoria accedida desde muchas tareas de vistas concurrentes.
Problema Un caché de diccionario simple accedido desde múltiples tareas falla o corrompe datos bajo carga.
Solución Envuelva el caché en un actor; los llamadores await sus métodos. Swift 6 entonces demuestra la ausencia de carreras en tiempo de compilación. "Recurro a un actor en lugar de un lock porque hace la carrera imposible, no solo improbable."
Rendimiento

Un feed de imágenes que mantiene 120fps

Concepto Scroll fluido = celdas ligeras, carga perezosa y sin trabajo en el hilo principal por frame.
Ejemplo Un feed social de fotos en resolución completa en una lista con scroll.
Problema Descodificar imágenes de 4000px para celdas de 300px eleva la memoria y pierde frames; Time Profiler muestra la descodificación en el hilo principal.
Solución Reduzca la resolución fuera del actor principal, cachée miniaturas descodificadas en un actor, use LazyVStack y dé a las filas identidad estable para que SwiftUI las reutilice. "Los frames perdidos casi siempre son trabajo de decodificación filtrándose al hilo principal — muévalo, cachee el resultado, y el frame rate se resuelve solo."
IA

Una funcionalidad de IA en el dispositivo

Concepto Ejecute inferencia en el dispositivo por privacidad, uso sin conexión y costo cero por llamada; vuelva al servidor solo cuando sea necesario.
Ejemplo Búsqueda inteligente que clasifica notas por significado, no solo por palabras clave.
Problema Enviar cada nota a un servidor es lento, costoso y un riesgo de privacidad.
Solución Incruste contenido con un pequeño modelo Core ML en el Neural Engine, almacene vectores localmente y clasifique por similitud coseno — exactamente lo que la propia búsqueda de esta guía hace en su navegador. "La inferencia en el dispositivo no es solo un argumento de privacidad — es costo marginal cero por consulta y sigue funcionando sin señal."
Capas

Clean Architecture y la regla de dependencia

Concepto Capas concéntricas (dominio → casos de uso → adaptadores → frameworks) con dependencias que apuntan solo hacia adentro; el dominio no importa nada.
Ejemplo Un motor de precios reutilizado en una app, un widget y un target de Swift del lado del servidor.
Problema Reglas de negocio enredadas con SwiftUI y URLSession no pueden reutilizarse ni probarse sin iniciar toda la app.
Solución Ponga las reglas en un paquete de dominio sin frameworks; las capas externas implementan sus protocolos y se conectan en una raíz de composición. Ahora el core es portátil y testeable unitariamente sin simulador.
Pruebas

Una estrategia de pruebas que realmente escala

Concepto Una pirámide: muchas pruebas unitarias rápidas, menos pruebas de integración, una capa delgada de pruebas de UI de extremo a extremo en flujos críticos.
Ejemplo Login, checkout y sincronización en una app en crecimiento con un presupuesto de CI de 20 minutos.
Problema Apoyarse en XCUITests lentos y frágiles para todo hace que CI sea lento y falle por tiempos, así que la gente deja de confiar en él.
Solución Empuje la lógica a view models inyectables cubiertos por pruebas unitarias rápidas; mantenga un puñado de pruebas de UI para los flujos principales; paralice en clones de simulador mediante un plan de pruebas. "Mantengo la forma de pirámide a propósito — las pruebas de UI verifican que los flujos críticos siguen funcionando de extremo a extremo, las pruebas unitarias cubren los casos límite de forma exhaustiva."