Revisión de Código Swift — CultivaFlow iOS

Revisor Experto Swift · CULTIVA IA · IA-Ingeniería-MLOps

🚫
BLOQUEADO — NO APTO PARA MERGE
Se han encontrado 3 issues CRITICAL y 5 HIGH que deben resolverse antes de fusionar con main.
PR: #47 feature/campaign-management → main
Rama: feature/campaign-management
Archivos: 3 .swift modificados
Líneas: +284 / -12
Fecha: 18 jun 2026, 14:37
Diagnóstico previo
✓ swift build — OK
⚠ swiftlint — 11 warnings
✓ swift test — 24/24 passed
✓ swift package resolve — OK
✗ swift-format — 6 style issues
3
Critical
5
High
4
Medium
12
Total issues
📄
CampaignService.swift
+142 / -5
Critical
Force unwrapping en respuesta de red — crash garantizado en error L:48
data!.campaigns fuerza el unwrap de un Optional que puede ser nil si el servidor devuelve 4xx/5xx o el cuerpo JSON es inválido. En producción esto provoca un crash sin posibilidad de recuperación.
● Antes ● Después
func fetchCampaigns() async throws { let result = try await api.get("/campaigns") campaigns = result.data!.campaigns }
func fetchCampaigns() async throws { let result = try await api.get("/campaigns") guard let data = result.data else { throw APIError.emptyResponse } campaigns = data.campaigns }
Critical
API key hardcodeada en código fuente L:12
La clave let apiKey = "ck_live_XXXXX..." está embebida directamente. Cualquiera con acceso al repositorio (o al binario descompilado con strings) obtiene la clave. Usar Keychain Services o inyección en tiempo de build desde el entorno CI.
● Antes ● Después
struct CampaignService { let apiKey = "ck_live_Xz7q9mNpLr..." let baseURL = "https://api.cultivaflow.io" }
struct CampaignService { private let apiKey: String init(keychain: KeychainService) { self.apiKey = keychain.string(for: .cultivaAPIKey) } let baseURL = "https://api.cultivaflow.io" }
High
Actualización de UI fuera del Main Actor L:87–94
self.loadingIndicator.stopAnimating() se llama desde una tarea concurrente sin garantizar el hilo principal. Todas las operaciones sobre UIView deben ejecutarse desde @MainActor.
● Antes ● Después
Task { await viewModel.refresh() self.loadingIndicator.stopAnimating() self.tableView.reloadData() }
Task { await viewModel.refresh() await MainActor.run { self.loadingIndicator.stopAnimating() self.tableView.reloadData() } }
📄
CampaignListViewModel.swift
+96 / -4
High
Ciclo de retención en closure escaping L:34
La closure captura self fuertemente en un contexto de larga duración. El ViewModel y el servicio se retienen mutuamente → memoria nunca liberada, leak en cada pantalla de campaña abierta.
● Antes ● Después
service.onUpdate = { self.campaigns = $0 self.objectWillChange.send() }
service.onUpdate = { [weak self] in guard let self else { return } self.campaigns = $0 self.objectWillChange.send() }
High
Task{} fire-and-forget sin cancelación L:58–65
Se lanza un Task {} sin almacenar la referencia. Si la vista se destruye antes de que acabe, la tarea continúa en background manteniendo referencias a objetos ya liberados. Usar concurrencia estructurada con TaskGroup o almacenar la referencia para cancelarla en deinit.
● Antes ● Después
func loadInitialData() { Task { try await service.fetchCampaigns() } }
private var loadTask: Task<Void, Never>? func loadInitialData() { loadTask = Task { [weak self] in try? await self?.service.fetchCampaigns() } } deinit { loadTask?.cancel() }
Medium
var donde bastaría let — 4 ocurrencias L:18, L:22, L:41, L:71
Cuatro propiedades declaradas con var nunca son reasignadas después de la inicialización. Usar let comunica inmutabilidad, mejora la legibilidad y permite optimizaciones del compilador.
📄
UserPreferencesManager.swift
+46 / -3
Critical
Token de sesión almacenado en UserDefaults L:29
UserDefaults.standard.set(token, forKey: "session_token") — UserDefaults se almacena en texto plano en el sandbox y puede leerse con acceso físico al dispositivo o en un entorno jailbroken. Los tokens de sesión deben ir siempre en Keychain Services con kSecAttrAccessibleWhenUnlockedThisDeviceOnly.
● Antes ● Después
func saveSessionToken(_ token: String) { UserDefaults.standard.set( token, forKey: "session_token") }
func saveSessionToken(_ token: String) { try keychain.set( token, key: .sessionToken, access: .whenUnlockedThisDeviceOnly ) }
High
API pública sin documentación /// L:8–45 (clase completa)
public class UserPreferencesManager expone 7 métodos public sin ningún doc-comment. El autocompletado en Xcode no mostrará ninguna descripción, dificultando el uso correcto de la API por el resto del equipo.
Medium
print() en código de producción — 3 llamadas L:31, L:38, L:44
Los print("Token guardado") quedan activos en release. Usar os.Logger con categoría y subsystem para logging estructurado que no se compile en release salvo configuración explícita.
Medium
Propiedades internas sin control de acceso explícito L:11–16
Cinco propiedades de implementación no declaradas private o fileprivate. Quedan accesibles desde cualquier fichero del módulo (internal por defecto), exponiendo detalles de implementación innecesariamente.
📋 Resumen de acciones requeridas