Francisco José García Navarro

Francisco José García Navarro

8 de junio de 2026

He revisado código iOS hecho con IA: lo que de verdad se rompe en producción

Pantalla con código Swift generado por IA durante una auditoría de código iOS
" Qué falla cuando una IA escribe tu app iOS, por qué compila igualmente y cómo auditarlo antes de desplegar. La visión de un arquitecto que lleva años metiendo las manos en código ajeno. "

En resumen

  • La IA te lleva al 70% de la solución; el 30% restante (errores, concurrencia, seguridad, arquitectura) es donde vive la producción.
  • El código vibe-coded parece terminado: compila, pasa tests y funciona en el simulador. Falla cuando se cae la red, se agota la memoria o hay miles de usuarios.
  • Las cuatro zonas frágiles: arquitectura sin capas, concurrencia silenciada, estado mal gestionado en SwiftUI y secretos sin cifrar.
  • Apple revisa tu app antes de publicarla: que compile no significa que App Review la apruebe.

Empiezo confesando algo que aquí no es tabú: yo uso IA para programar todos los días. Vengo de ChatGPT y desde que salió Claude Code me mudé sin mirar atrás. En mis proyectos personales me ahorra horas, me desatasca, me escribe el código repetitivo que no quiero escribir. No soy el típico que te va a decir que la IA es mala. Es una herramienta brutal.

Pero hay una diferencia enorme entre usar una IA para programar y dejar que una IA programe por ti. Y esa diferencia no se ve el primer día. Se ve a los tres meses, cuando la app que compilaba, arrancaba y "funcionaba" se convierte en un muro que nadie puede mover.

Lo he visto varias veces ya. Me llega un founder —normalmente alguien sin perfil técnico, con buena idea de producto— que ha construido su app iOS con una IA. Tiene un MVP que funciona en la demo. Quiere escalar: añadir features, meter a más gente, soportar más usuarios. Y no puede. El código se le resiste. Cada cambio rompe tres cosas. Nadie entiende por qué la app hace lo que hace. Han llegado al techo, y el techo estaba mucho más bajo de lo que parecía.

Si eres Tech Lead y estás a punto de heredar —o ya has heredado— una base de código iOS generada con IA, esto es lo que de verdad te vas a encontrar. No teoría. Patrones concretos que rompen en producción, ordenados por dónde más daño hacen.

El espejismo de la productividad: por qué el código vibe-coded parece terminado y no lo está

Andrej Karpathy bautizó "vibe coding" a principios de 2025: programar dejándote llevar, olvidando que el código siquiera existe. Lo dijo, importante, para proyectos de fin de semana desechables. El problema empieza cuando ese mismo flujo —pídele, acéptalo, sigue— se usa para algo que va a producción y va a tener que mantenerse.

Addy Osmani, que lidera experiencia de desarrollador en Google Chrome, lo resumió mejor que nadie con lo que llama el problema del 70%: la IA te lleva con asombrosa facilidad hasta el 70% de la solución. El último 30% —los casos límite, el manejo de errores, la integración con sistemas de producción, la seguridad, las claves de API— es justo donde vive la producción. Y en iOS yo añado tres que pesan especialmente: la accesibilidad, la seguridad de la concurrencia y el encaje arquitectónico. En mi experiencia, para un perfil senior, cerrar ese 30% revisando código ajeno suele ser más lento que haberlo escrito uno mismo.

Ese 70% es exactamente lo que engaña al founder no técnico. La app compila. Pasa los tests que la propia IA escribió. Hace lo que tiene que hacer en el simulador. Todas las señales que esa persona sabe interpretar dicen "terminado". Pero el 30% que falta no da error en la demo: da error cuando se cae la red, cuando el dispositivo se queda sin memoria, cuando hay 10.000 usuarios en vez de uno, cuando dos hilos tocan el mismo dato a la vez. El espejismo no es que el código esté mal escrito. Es que parece completo sin estarlo.

Martin Fowler tiene una imagen que me parece perfecta para esto: la IA produce "la media de internet", una agregación de patrones de millones de repos. No produce código que encaje en tu arquitectura ni en tus convenciones. Produce lo más probable. Y lo más probable, en iOS, suele ser código con forma de UIKit en un proyecto SwiftUI, MVC en una base MVVM, y singletons por todas partes.

Vamos a las cuatro zonas donde esto se concreta.

A partir de aquí entro en el detalle técnico, porque prefiero demostrarte lo que se rompe antes que pedirte que me creas. Si eres founder o responsable de producto y no programas en Swift, no necesitas seguir cada ejemplo de código: quédate con la idea de cada sección y, si quieres, salta directo a cuándo merece la pena una auditoría externa, donde traduzco todo esto a señales que reconoces sin abrir Xcode —cada cambio rompe cosas que parecían no tener relación, nadie en tu equipo sabe explicar por qué la app se comporta como lo hace, o manejáis datos sensibles y nadie ha revisado la seguridad—. Si eres técnico, sigue conmigo.

Zona Lo que la IA entrega Lo que falla en producción
Arquitectura La vista habla directamente con la red, sin capas Imposible de testear y escalar; crash al primer timeout
Concurrencia y memoria nonisolated(unsafe) y @MainActor para silenciar Swift 6 Data races vivos, crashes irreproducibles, fugas de memoria
Estado en SwiftUI @ObservedObject creado dentro de la vista Estado que se pierde de forma aleatoria al recrearse la vista
Seguridad Tokens en UserDefaults, secretos hardcodeados Datos sensibles en texto plano, exposición legal bajo RGPD

Arquitectura: lo primero que la IA ignora

El patrón número uno que me encuentro: la vista habla directamente con la red. No hay capa de servicio, no hay repositorio, no hay separación. El View hace el URLSession, parsea el JSON, y mete la lógica de negocio en el body. Funciona. Es imposible de testear, imposible de reutilizar e imposible de mantener.

// Lo que escribe la IA cuando le pides "una pantalla que muestre los pedidos"
struct OrdersView: View {
    @State private var orders: [Order] = []

    var body: some View {
        List(orders) { order in
            Text(order.title)
        }
        .task {
            // Red, parsing y lógica, todo dentro de la vista
            let url = URL(string: "https://api.example.com/orders")!
            let (data, _) = try! await URLSession.shared.data(from: url)
            orders = try! JSONDecoder().decode([Order].self, from: data)
        }
    }
}

Fíjate además en los dos try!. La IA elimina el manejo de errores porque es lo que hace que el código "funcione" más rápido. En la demo nunca falla la red. En producción, el primer timeout es un crash.

A esto se suman cosas que Paul Hudson documenta muy bien en su repaso de qué hay que corregir en código Swift generado por IA: meter decenas de tipos en un único fichero —garantía de tiempos de compilación eternos—, abusar de GeometryReader con frames fijos donde no tocan, partir las vistas en propiedades computadas en lugar de en vistas separadas (lo que rompe la invalidación inteligente de @Observable), y usar onTapGesture donde corresponde un Button —un fallo de accesibilidad de manual, porque VoiceOver no lo trata como elemento interactivo—.

Y un clásico de los proyectos vibe-coded que heredo: la IA crea una clase nueva de red cada vez que la necesita, en vez de reutilizar el cliente que ya existía. Acabas con cuatro maneras distintas de hacer una petición HTTP en la misma app. Cada una con su propio manejo de errores, o sin ninguno.

Nada de esto da la cara en la demo. Todo da la cara cuando intentas crecer. Aquí es donde el founder se atasca y donde necesita un iOS senior que se integre en tu equipo para reconstruir los cimientos sin tirar el producto.

Concurrencia y memoria en Swift: los fallos que no ves hasta producción

Esta es, con diferencia, la zona más frágil. Y es específica de Swift, porque la concurrencia estricta de Swift 6 —si has visto lo nuevo de Swift 6.2— obliga a tomar decisiones que las IA aciertan mal con una frecuencia preocupante.

Paul Hudson lo describe casi como un tic mecánico: cuando la IA se topa con un problema de concurrencia, ves aparecer DispatchQueue.main.async un número irrazonable de veces, resucitado de tiempos pre-async/await. Y Task.sleep(nanoseconds:) donde debería ir el Task.sleep(for:) moderno.

Pero el patrón que más me preocupa, el que de verdad me hace levantar la ceja en una auditoría, es este:

// La IA se encuentra un error de data race de Swift 6 y lo "soluciona" así
nonisolated(unsafe) static var shared = AppState()

nonisolated(unsafe) y @preconcurrency son válvulas de escape legítimas para casos muy concretos. Pero la IA las usa para silenciar el error del compilador, no para arreglar el problema que el compilador estaba señalando. Y aquí está el detalle clave que tienes que entender como Tech Lead: la concurrencia estricta de Swift 6 es tu red de seguridad. Detecta data races reales en tiempo de compilación, antes de que lleguen a producción. Cuando la IA mete un nonisolated(unsafe) para que compile, no está resolviendo nada: está cortando la red de seguridad y dejando el data race vivo, esperando a manifestarse como un crash imposible de reproducir en cuanto haya concurrencia real.

Lo mismo con @MainActor: lo verás aplicado a clases enteras "para que el compilador se calle", metiendo en el hilo principal trabajo que no tiene por qué estar ahí. Y los ciclos de retención clásicos en closures siguen apareciendo —self capturado fuerte donde tocaba [weak self]—, con sus correspondientes fugas de memoria.

Donny Wals, que ha escrito el libro sobre concurrencia en Swift y consultoría de migraciones a Swift 6, lo resume sin rodeos: una IA con buenos guardrails te genera código razonable, layouts sensatos, flujos decentes. Pero no sabe qué se siente al usar tu app en el mundo real. Puede generar slop muy rápido. Y tú no quieres construir slop.

Y para quien no programa: esto son los crashes que tu equipo no consigue reproducir, los que aparecen y desaparecen en manos de usuarios reales y acaban en reseñas de una estrella sin que nadie sepa señalar la causa. No es un detalle de ingeniería: es soporte ardiendo y usuarios que se marchan.

Gestión de estado en SwiftUI: el patrón que la IA repite mal

Si hay un error que la IA comete una y otra vez en SwiftUI, es confundir cuándo una vista posee un objeto y cuándo solo lo observa. Suena tonto. No lo es.

// El error que pasa code review tranquilamente
struct WatchlistView: View {
    @ObservedObject private var viewModel = WatchlistViewModel()  // ⚠️
    // ...
}

Antoine van der Lee (SwiftLee) lo explica con claridad: es inseguro crear un @ObservedObject dentro de una vista, porque SwiftUI puede crear y recrear vistas en cualquier momento. Aquí tiene que ir @StateObject, que es quien garantiza que el objeto sobrevive a las reconstrucciones de la vista. Con @ObservedObject, el view model se recrea de cero en momentos impredecibles y el estado se pierde de forma aleatoria.

Y aquí está lo peligroso, lo que lo convierte en una bomba de relojería: funciona durante las pruebas ligeras. Funciona en la demo. Y luego falla de forma aleatoria en producción. Es lo bastante sutil como para pasar una revisión de código sin que nadie lo note.

El segundo patrón es tratar la macro @Observable (iOS 17+) como un reemplazo directo de ObservableObject. No lo es. Jesse Squires documentó un caso real de fuga de memoria precisamente por esto: como @Observable se usa con @State y no con @StateObject, su inicializador se dispara en cada reconstrucción de la vista. El resultado fueron instancias del modelo acumulándose en memoria indefinidamente, todas vivas, todas observando notificaciones del sistema y compitiendo por escribir en UserDefaults. Al reabrir la app, cargaba datos básicamente aleatorios. Migrar @Observable "a lo bruto" sin entender la diferencia de inicialización no es un refactor: es un bug esperando a pasar.

En la práctica, esto es un usuario que pierde lo que estaba haciendo, ajustes que se resetean solos, datos que aparecen mal al reabrir la app. Fallos intermitentes, imposibles de reproducir a la primera, que erosionan la confianza justo cuando empiezas a tener usuarios de verdad.

Seguridad y datos: lo que se cuela cuando nadie revisa

La IA optimiza para que la cosa funcione, y la ruta más corta a que funcione casi nunca es la ruta segura. Lo que me encuentro, en orden de frecuencia:

  • Claves API y secretos hardcodeados en el código fuente. Extraíbles del .ipa compilado por cualquiera con cinco minutos y curiosidad.
  • Tokens en UserDefaults en vez de en el Keychain. UserDefaults no está cifrado y se lee directamente de un backup del dispositivo. El token de sesión de tu usuario, en texto plano, en una copia de seguridad.
// Lo que escribe la IA
UserDefaults.standard.set(authToken, forKey: "authToken")  // ⚠️ sin cifrar

// Lo que debería ser: Keychain, con accesibilidad correcta
// kSecAttrAccessibleWhenUnlockedThisDeviceOnly, no kSecAttrAccessibleAlways
  • Sin certificate pinning, y excepciones de ATS (NSAllowsArbitraryLoads) añadidas para que funcione un endpoint de desarrollo y nunca eliminadas.
  • PII en los logs, escrita con print o os_log a visibilidad por defecto, visible para cualquiera con el dispositivo conectado.

No hay un estudio cuantitativo serio específico de iOS sobre esto, y prefiero decírtelo a inventarme una cifra. Pero los análisis sobre código generado por IA en general son consistentes: más velocidad de escritura, y proporcionalmente más hallazgos de seguridad. La frase que mejor lo captura es de un análisis de Apiiro sobre repos enterprise: la IA arregla las erratas y crea las bombas de relojería. En una app que maneja datos personales de usuarios europeos, eso no es deuda técnica: es exposición legal directa bajo el RGPD.

Antes de producción está App Review: lo que Apple rechaza aunque compile

Hay una capa del espejismo que llega antes incluso de que tus usuarios toquen la app: App Review. Que algo compile y arranque en el simulador no significa que Apple lo vaya a aprobar.

Un caso adyacente que también me ha llegado: apps que Apple rechaza de plano. En mi experiencia, los rechazos que he visto venían de apps que no eran nativas —wrappers de una web, básicamente— y chocaban con la Guideline 4.2 (Minimum Functionality). El criterio de Apple es claro: una app tiene que aportar algo más que una página web reempaquetada. Si la experiencia no se diferencia lo suficiente de abrir Safari, no entra. Y ojo, esto no significa que no puedas usar tecnología web o frameworks multiplataforma; significa que el resultado tiene que sentirse como una app nativa de verdad, con navegación, notificaciones, comportamiento offline y un encaje real en el sistema.

El código nativo generado por IA no suele caer en la 4.2 —es nativo—, pero puede caer en rechazos por otras razones igual de evitables, y aquí es donde conecta con todo lo anterior. La IA optimiza para "que funcione en el simulador", no para "que pase App Review". Eso se traduce en uso de APIs privadas o ya deprecadas que el revisor detecta, patrones de interfaz que ignoran las Human Interface Guidelines (la sección 4.0, Design, de las guidelines), y ausencia total de accesibilidad —recuerda el onTapGesture en vez de Button del que hablábamos: además de romper VoiceOver, es el tipo de detalle que puede costarte un rechazo—.

El patrón de fondo es el mismo de siempre: la IA te entrega algo que parece terminado, y "terminado" para Apple incluye un montón de cosas que la IA no sabe que tiene que comprobar.

Cómo auditar código iOS generado por IA antes de desplegar

Si vas a heredar una base de código vibe-coded, este es el recorrido mínimo que hago yo antes de dar nada por bueno. Sirve igual para validar tu propio código generado con IA.

  1. Busca los silenciadores del compilador. Un grep de nonisolated(unsafe) y @preconcurrency. Cada aparición es un punto rojo: alguien (o algo) hizo callar a la concurrencia estricta de Swift 6 en vez de resolver el problema. Trátalos como prioridad uno.
  2. Audita el almacenamiento de secretos. Busca UserDefaults con cualquier cosa que huela a token, sesión o credencial. Busca Bearer, API_KEY, sk_, Authorization hardcodeados. Busca NSAllowsArbitraryLoads.
  3. Revisa la propiedad del estado. Cada @ObservedObject inicializado dentro de una vista (= AlgoViewModel()) es casi siempre un @StateObject mal puesto. Cada migración a @Observable hecha sin pensar en la inicialización es sospechosa.
  4. Mira las capas. ¿La vista llama a la red directamente? ¿Hay lógica de negocio en el body? ¿Cuántas formas distintas de hacer una petición HTTP conviven en el proyecto?
  5. Cuenta el manejo de errores. Cada try! y cada force unwrap (!) en una ruta de producción es un crash esperando su turno. La IA los mete porque acortan el camino a "funciona".
  6. Comprueba lo que la IA decidió NO escribir. Estado vacío, fallo de red, red lenta, relanzamiento en segundo plano, Dynamic Type al máximo, ruta de VoiceOver, idiomas RTL. Ese es el 30% de Osmani, y es donde vive la producción.
  7. Activa la concurrencia estricta de Swift 6 en CI y trata cada diagnóstico como información, no como ruido a silenciar.
  8. Pásale el filtro de App Review. Busca APIs privadas o deprecadas, comprueba que la interfaz sigue las Human Interface Guidelines y que la app aporta funcionalidad nativa real. Revisa accesibilidad básica: VoiceOver, Dynamic Type, elementos interactivos correctos. Es más barato encontrarlo tú que recibir el rechazo de Apple.

La regla de oro, que tomo prestada de Simon Willison: no subas a producción código que no sepas explicar a otra persona. Si nadie en el equipo puede explicar qué hace una pieza y por qué, no está terminada, por mucho que compile.

Cuándo merece la pena una auditoría externa

No siempre hace falta. Si tienes un equipo iOS senior con ancho de banda para revisar a fondo, hazlo en casa. La IA, bien encauzada y con revisión senior de verdad, es una aceleradora extraordinaria —lo es para mí cada día—.

Merece la pena traer a alguien de fuera cuando se dan algunas de estas señales: heredaste una app construida por un perfil no técnico y no puedes escalarla; Apple te ha rechazado la app y no tenéis claro cómo dejarla conforme; cada cambio rompe cosas que parecían no relacionadas; nadie en el equipo puede explicar por qué la app se comporta como se comporta; la app maneja datos sensibles o pagos y nadie ha revisado la seguridad; o vais con prisa hacia producción y necesitáis una segunda opinión senior antes de que el coste de los errores se dispare.

Lo que hace una IA en minutos puede tardar meses en arreglarse si nadie miró el código mientras se escribía. El ahorro de hoy es, demasiadas veces, la deuda técnica de dentro de tres meses. No porque la herramienta sea mala, sino porque la productividad sin revisión es una ilusión que se cobra intereses.

Preguntas frecuentes

La IA te lleva con facilidad hasta el 70% de la solución: la app compila, pasa los tests y funciona en el simulador. El 30% restante —casos límite, manejo de errores, concurrencia, seguridad y encaje arquitectónico— es justo donde vive la producción, y no da la cara hasta que se cae la red, se agota la memoria o hay miles de usuarios a la vez.

Es una válvula de escape que silencia los errores de la concurrencia estricta de Swift 6. La IA la usa para que el código compile, no para resolver el problema. Cuando aparece, casi siempre significa que hay un data race real sin arreglar, esperando a manifestarse como un crash imposible de reproducir.

No. UserDefaults no está cifrado y se lee directamente desde un backup del dispositivo. Los tokens, credenciales y secretos deben guardarse en el Keychain con la accesibilidad correcta. Guardar un token de sesión en UserDefaults deja datos sensibles en texto plano.

Busca silenciadores del compilador, revisa el almacenamiento de secretos, comprueba la propiedad del estado, mira si la vista llama directamente a la red, cuenta los force unwraps en rutas de producción y verifica lo que la IA decidió no escribir: estados vacíos, fallos de red y accesibilidad.

Cuando heredas una app construida por un perfil no técnico y no consigues escalarla, cuando Apple la ha rechazado, cuando cada cambio rompe cosas no relacionadas, cuando nadie en el equipo puede explicar por qué la app se comporta como lo hace, o cuando maneja datos sensibles o pagos y nadie ha revisado la seguridad.

¿Has heredado una app iOS hecha con IA y no consigues escalarla, o quieres validar tu código antes de desplegar? En AtalayaSoft hacemos auditoría de código iOS generado por IA desde la experiencia real de arquitectura en apps con millones de usuarios. Te decimos qué está roto, qué es urgente y cómo arreglarlo sin tirar tu producto.

Hablemos →

Sobre el autor
Francisco José García Navarro

Francisco José García Navarro

Francisco José García Navarro es cofundador y Arquitecto iOS Senior de AtalayaSoft, con más de 25 años en desarrollo de software y 11+ en iOS nativo. A lo largo de su carrera ha trabajado con clientes de alto perfil como Zara (Inditex), Banco Santander, AXA, El País, National Geographic, Fox International Channels y el Museo Thyssen-Bornemisza.