App de iOS
La app de iOS de Closegram es una aplicación SwiftUI lista para producción ubicada en apps/ios/. Está dirigida a iOS 15+ y requiere Xcode 15+.
Arquitectura: MVVM + Redux + Clean Architecture
Cada funcionalidad está organizada en tres capas:
Feature/
├── Data/ → Repository implementations, Services
├── Domain/ → Business models, protocols
└── Presentation/ → Redux Store, ViewModels (UI only), Views
Flujo de datos unidireccional:
- La vista despacha una
ActionalStore - El
Reducer(función pura) produce un nuevoStateinmutable - Para trabajo asíncrono, el
Storellama a unRepository - El repositorio coordina uno o más
Services(Apollo, Firebase, Stripe…) - El store actualiza el estado → SwiftUI vuelve a renderizar
Inyección de dependencias
Los servicios y repositorios se inyectan mediante un DIContainer seguro para hilos:
@Injected private var authRepository: AuthRepositoryProtocol
Todo se registra una sola vez en appApp.swift al iniciar.
Navegación
SwiftUI puro, sin puente a UIKit: MainTabView es un NavigationStack(path: $router.path) que envuelve un TabView, con cada una de las 5 pestañas (Updates/Calls/Communities/Chats/Settings) teniendo su propio NavigationStack interno para navegación push por pestaña, más .navigationDestination(for: AppRoute.self) para las 4 rutas entre pestañas (.chat, .contactInfo, .archivedChats, .userProfile) manejadas por AppRouter. Al ser SwiftUI puro, el entorno (todos los @EnvironmentObject) se propaga automáticamente por el árbol — ya no hay una lista manual de reasignación por ruta que mantener sincronizada, que era la fuente recurrente de crashes en la implementación anterior basada en UIViewControllerRepresentable.
Módulos de funcionalidades
| Funcionalidad | Estado | Aspectos destacados |
|---|---|---|
| Auth | ✅ 95% | OTP por teléfono, Apple Sign In, Google Sign In, restablecimiento de contraseña — la actualización de token y eliminar cuenta son las brechas restantes, ver abajo |
| Chat | ✅ 100% | Suscripciones en tiempo real, compartir ubicación, reacciones, chats grupales, indicadores de escritura, selector de GIF |
| Calls | ✅ 100% | LiveKit WebRTC, CallKit, Picture-in-Picture, roles de orador/espectador |
| Search | ✅ 100% | Con debounce (500ms), búsqueda de usuarios + conversaciones, filtros en línea/verificado |
| Profile | ✅ 100% | Ver/editar el perfil propio y el de otros, subir foto/portada |
| Payment | ✅ 90% | Integración con Stripe, agregar/quitar/predeterminar métodos de pago |
| CoinPackage | ✅ 100% | Explorar y comprar paquetes de monedas |
Pendientes conocidos
Crítico (Chat): carga de contenido multimedia (imágenes, audio, documentos, encuestas), edición/reenvío de mensajes.
Crítico (Auth): actualización de token (AuthService.refreshToken() es un stub TODO literal), eliminar cuenta (AuthRepository.deleteAccount() llama a una mutation real del backend, pero el ProfileRepository.deleteAccount() que en realidad dispara la UI del lado de Configuración es un stub que lanza ProfileError.featureNotSupported, y ninguno de los dos caminos tiene un botón/vista real conectado).
Medio: búsqueda de mensajes (pendiente en el backend de GraphQL), caché local de pagos.
No implementado: modo oscuro, soporte para iPad, accesibilidad VoiceOver/Dynamic Type.
Infraestructura principal
| Componente | Descripción |
|---|---|
DIContainer | Contenedor de inyección de dependencias seguro para hilos |
CacheCoordinator | Caché unificado con TTL + expulsión: .memory 10MB, .images 50MB, .disk 100MB |
ErrorHandler | Manejo centralizado de errores con reintento automático, soporte de reautenticación, notificaciones al usuario |
NetworkMonitor | Conectividad en tiempo real mediante NWPathMonitor + publishers de Combine |
MessageQueue | Cola de mensajes offline persistente con reintento por backoff exponencial (máximo 3) |
Logger | Sistema de logging unificado |
Servicios externos (iOS)
| Servicio | Propósito |
|---|---|
| Firebase | Autenticación (teléfono, Google), Analytics, Crashlytics |
| LiveKit | Llamadas de audio/video WebRTC |
| Stripe | Integración de hoja de pago |
| Google Sign-In | Inicio de sesión OAuth |
| Apple Sign In | Autenticación nativa |
| Giphy | Selector de GIF/stickers en el chat |
| CallKit | Interfaz nativa de llamadas de iOS |
| VoIP push | Alertas de llamadas en segundo plano |
Networking
Las queries, mutations y suscripciones de GraphQL se ejecutan mediante Apollo iOS. Los tipos de Swift generados viven en el módulo ClosegramGraphQL (paquete SwiftPM packages/apollo-swift, generado desde packages/graphql con npm run codegen:ios). La estrategia de caché es adaptativa — en línea usa .returnCacheDataAndFetch, sin conexión usa .returnCacheDataDontFetch.
Soporte sin conexión (97% completo)
Los usuarios pueden:
- Enviar mensajes sin conexión (se encolan y reintentan automáticamente al reconectar)
- Ver conversaciones y mensajes en caché
- Ver métodos de pago en caché
- Ver un
OfflineBannervisual con tres estados: sin conexión → reconectando → conectado
Localización
i18n con tipado seguro y cambio de idioma en tiempo de ejecución (sin necesidad de reiniciar):
Text(LocalizedString.Chat.Message.send.localized)
LocalizedString.Chat.Group.participants(count: 5) // "5 participants"
LocalizationManager.shared.setLanguage(.spanish)
Idiomas: inglés (por defecto), español — más de 230 claves en 11 categorías.
Configuración
cd apps/ios/app
open app.xcodeproj
# File → Packages → Resolve Package Versions
Configura las variables de entorno en el scheme (Run → Arguments):
STRIPE_PUBLISHABLE_KEY = pk_test_...
GIPHY_API_KEY = ...
API_HOST = <la IP local de tu máquina> # backend local, solo dispositivo físico
API_ENVIRONMENT = development # opcional: apunta los builds Debug a dev.api.closegram.com en vez de local
A qué entorno de backend (local / development / production) apunta un build, y cómo dirigir cada uno desde Xcode y Fastlane, se cubre en Entornos de despliegue.
Regenerar el código de GraphQL
Cada vez que cambie packages/graphql/schema/schema.web.graphqls o cualquier operación .graphql bajo packages/graphql/operations/Web/**:
npm run codegen:schema # regenera el schema contra el que apollo-swift genera código
npm run codegen:ios # regenera packages/apollo-swift/Sources/ClosegramGraphQL
y luego reconstruye. CI lo hace cumplir — el job test en .github/workflows/ios-ci.yml corre swift run codegen (desde packages/apollo-swift) y falla el build si el Swift generado está desactualizado respecto a lo versionado, así que código generado obsoleto no puede fusionarse silenciosamente.
Ejecutar pruebas
# Via Fastlane
SKIP_GIT_CHECK=true bundle exec fastlane test
# Via xcodebuild
xcodebuild test -scheme app -destination 'platform=iOS Simulator,name=iPhone 15 Pro'
Corre en cada push/PR vía .github/workflows/ios-ci.yml (runner de macOS, Xcode 26.1 — requerido por el IPHONEOS_DEPLOYMENT_TARGET = 26.1 de este proyecto). Hoy la cobertura de pruebas está concentrada por completo en AuthTests (9 archivos) — pruebas de reducer, store y use-case para los flujos de login/registro/contraseña, todas pasando en CI. Ninguna otra funcionalidad tiene cobertura de pruebas todavía; ver Migración de paridad de iOS.