Reimaginando la arquitectura iOS en la era de Swift 6.2

Arquitectura cleancode IOS SwiftUI

Adoptamos SwiftUI como base de desarrollo en iOS y rediseñamos nuestra arquitectura para adaptarnos a los cambios de Swift 6, especialmente en concurrencia estricta. El resultado es una arquitectura moderna basada en principios SOLID, concurrencia estructurada con async/await, alta testabilidad, escalabilidad y mínima dependencia de librerías externas.

1. El problema que queríamos resolver
2. Controlando la concurrencia con Swift 6.2
3. Inyección de dependencias con macros nativas
4. Patrones de diseño: el papel del Strategy
5. El server manager: seguridad y configurabilidad
6. Testabilidad: el beneficio directo del desacoplamiento
7. Conclusión

Cómo el equipo iOS de O2O rediseñó desde cero su arquitectura para abrazar la concurrencia estricta, los principios SOLID y la escalabilidad real.

Ante el establecimiento de SwiftUI como marco de desarrollo en iOS y los grandes cambios introducidos en las últimas versiones del lenguaje — especialmente la concurrencia estricta de Swift 6 y Swift 6.2 — decidimos dar un paso al frente y repensar la arquitectura desde sus cimientos.

El resultado es una arquitectura nueva, construida sobre seis pilares fundamentales que guían cada decisión técnica del equipo:

  • Cumplimiento de principios SOLID: Responsabilidad única, Abierto/Cerrado, Sustitución de Liskov, Segregación de Interfaces e Inversión de Dependencias.
  • Test unitarios que validan el funcionamiento del código desde sus componentes más pequeñas.
  • Concurrencia estricta implementada con Swift 6.2.
  • Async/await para tareas asíncronas con concurrencia estructurada, manteniendo la legibilidad del código complejo.
  • Arquitectura escalable que facilita el desarrollo de nuevas funcionalidades sin comprometer las existentes.
  • Mínimas dependencias posibles a librerías de terceros.

 

El problema que queríamos resolver

Uno de los errores más comunes en proyectos con varios desarrolladores es que cada uno tenga una interpretación diferente de las responsabilidades de cada capa. El error más frecuente: lógica de negocio en los view models o presenters. Nuestra primera prioridad era precisamente evitar esa problemática.

Lo primero que establecimos fueron las capas de nuestra arquitectura: la vista con su view model para controlar la lógica de presentación; el patrón Use Case para encapsular la lógica de negocio; y en la capa de datos, el patrón Repository para obtener información de distintas fuentes, acompañado de un server manager para gestionar la comunicación con las APIs.

En la capa de use case creamos una clase padre con funciones genéricas para que todos los casos de uso siguiesen la misma metodología: una función execute para use cases que no necesitan comunicarse con la capa de datos, y otra que sí lo hace, cambiando de contexto para trabajar en un hilo distinto al principal.

 

Diagrama de capas: Use Case

 

La capa de dominio trabaja con objetos de negocio con capacidad de mapearse a Data Transfer Objects, garantizando así la separación de responsabilidades entre capas.

Controlando la concurrencia con Swift 6.2

Una vez establecidas las capas, el siguiente reto era controlar bien la concurrencia. Mediante global actors personalizados, establecimos que la capa de datos funcionaría en un contexto de ejecución propio, distinto al del resto de la aplicación.

Tanto el view model como los use cases implementan el patrón async/await en las funciones genéricas de sus clases base, lo que permite mantener la concurrencia estructurada y evitar los data races en el código.

Flujo de concurrencia con global actors y async/await

 

Inyección de dependencias con macros nativas

En proyectos grandes, la sobre instanciación de objetos durante el desarrollo de nuevas funcionalidades es un problema real. Para evitarlo, optamos por la Inyección de Dependencias: las dependencias se crean en un único punto de la aplicación y se inyectan en las clases correspondientes mediante macros — anotaciones personalizadas construidas a partir de macros nativas de iOS.

Inyección de dependencias mediante macros de Swift

 

Patrones de diseño: el papel del Strategy

Durante el desarrollo buscamos hacer el código lo más abstracto posible para el desarrollador, que cumpliese con nuestras directrices y que la curva de aprendizaje fuese baja. El patrón que más utilizamos es el Strategy.

Este patrón de comportamiento permite definir una familia de algoritmos, colocarlos en clases separadas y hacer sus objetos intercambiables. Nos resultó especialmente útil para facilitar la instanciación de managers de librerías de terceros y para establecer reglas de validación de campos de forma homogénea entre proyectos.

Implementación del patrón Strategy para managers y validaciones

 

El server manager: seguridad y configurabilidad

La última pieza de la cadena es el server manager, invocado desde el repositorio según el origen de los datos. Estos managers se utilizan para obtener información de APIs externas, y aplican una capa de seguridad en la construcción de la URL — los endpoints se obtienen mediante encriptación.

Arquitectura del server manager con endpoints encriptados

 

Esta capa es altamente configurable. Contamos con funciones básicas de CRUD, caché en disco, en memoria o ambas, e interceptors para tokens y su refresco, SSL pinning, logging de peticiones y errores, reintentos automáticos ante fallos, y la posibilidad de crear interceptors propios e inyectarlos cuando se necesiten.

Testabilidad: el beneficio directo del desacoplamiento

La gran ventaja de una arquitectura tan desacoplada, basada en protocolos y respetuosa con los principios SOLID, es que permite crear tests de forma sencilla y rápida. Cada capa puede probarse de forma independiente, con mocks claros y sin efectos secundarios inesperados.

Ejemplo de tests unitarios sobre los server managers

 

Conclusión

Repensar la arquitectura no es un lujo — es una inversión. Una base sólida, concurrente y bien testada es lo que permite escalar un producto iOS sin acumular deuda técnica.

Esta arquitectura ha supuesto un antes y un después en la forma en que el equipo iOS de O2O afronta el desarrollo. La combinación de SwiftUI, Swift 6.2, SOLID, patrones de diseño bien elegidos y una capa de red segura y configurable nos ha dado la confianza para escalar nuestros proyectos con solidez.

 

Referencias

Compartir artículo