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
-
CEliminar la API key hardcodeada de
CampaignService.swifte inyectarla desde Keychain. Nunca comprometer credenciales en git. -
CMigrar
session_tokende UserDefaults a Keychain con accesowhenUnlockedThisDeviceOnly. -
CReemplazar
result.data!conguard lety lanzar un error tipado en lugar de crashear. -
HEnvolver actualizaciones de
UIView/UITableViewenawait MainActor.run {}o anotar la función con@MainActor. -
HAñadir
[weak self]a la closureservice.onUpdatepara evitar el ciclo de retención entre ViewModel y servicio. -
HGuardar la referencia del
Task {}enloadTasky cancelarla endeinitpara evitar fugas de concurrencia. -
HDocumentar los 7 métodos públicos de
UserPreferencesManagercon comentarios///antes de considerar la API estable. -
MCambiar los 4
varque nunca mutan alet; reemplazar losprint()poros.Logger; añadirprivate/fileprivatea propiedades internas.