Apps de demo por módulo: arrancar una feature sola
Una app mínima por módulo que arranca directamente en la feature con datos falsos: qué prueba que un preview no prueba, cómo se construyen los dobles sin duplicarlos, y por qué la demo funciona además como test de arquitectura ejecutable.
La modularización se justifica casi siempre por los tiempos de compilación, pero su consecuencia más transformadora es otra y aparece más tarde: una feature que vive en su propio módulo se puede arrancar sola. No dibujar en un lienzo, no simular en una vista previa: arrancar, en un simulador, como aplicación completa, con su ciclo de vida, su navegación, sus animaciones y su teclado, sin pantalla de login, sin sincronización inicial, sin las cinco pulsaciones que hacen falta para llegar hasta ella. Eso es una app de demo: un target de cuarenta líneas por módulo cuyo body es la vista raíz de la feature con dependencias falsas inyectadas. Cuesta una tarde montarla y cambia la forma en que se desarrolla, porque el bucle entre escribir una línea y verla funcionando pasa de minutos a segundos y porque cada estado difícil de alcanzar —el error de red, la lista vacía, el usuario sin permisos— deja de requerir una conspiración de circunstancias y se convierte en un valor que se escribe.
- Construir un
targetde demo mínimo que arranque una feature aislada en el simulador. - Distinguir qué verifica una demo ejecutable y qué no puede verificar un
preview. - Diseñar dobles de datos reutilizables entre demo,
previewy tests sin duplicarlos. - Usar la demo como prueba ejecutable de que el módulo está realmente desacoplado.
Cuarenta líneas que cambian el bucle
Una app de demo no es un proyecto aparte ni un segundo repositorio. Es un target de aplicación dentro del mismo espacio de trabajo, que depende únicamente del módulo de la feature y de los dobles, y cuyo contenido cabe en una pantalla.
// Demos/PerfilDemo/PerfilDemoApp.swift
import SwiftUI
import FeaturePerfil
import DominioFalso
@main
struct PerfilDemoApp: App {
var body: some Scene {
WindowGroup {
NavigationStack {
PerfilView(
cliente: ClienteAPIFalso.conDatos,
usuario: .ejemplo
)
}
}
}
}
Lo que hace valiosa a esta demo no es lo que contiene sino lo que no contiene. No hay contenedor de persistencia, no hay analítica, no hay comprobación de sesión, no hay carga remota de configuración. Arranca en el estado que quieres examinar porque no hay ningún camino que recorrer para llegar hasta él. Y su binario es pequeño, así que el ciclo de editar, compilar y ejecutar dura lo que dura compilar un módulo.
Hay una segunda propiedad, menos evidente y más severa: si la demo compila, el módulo está desacoplado. No es una opinión sobre el diseño ni el resultado de una revisión; es una afirmación verificada por el compilador cada vez que la construyes. Si alguien introduce en la feature una dependencia hacia el target de la app, la demo deja de enlazar de inmediato. Es un test de arquitectura que no hay que escribir, que no se olvida de ejecutar y que produce un error con posición exacta.
flowchart LR subgraph normal[Sin demo] App1[App completa] --> Login[Pantalla de login] Login --> Sync[Sincronizacion inicial] Sync --> Tabs[Barra de pestanas] Tabs --> Vista1[La vista que estas editando] end subgraph demo[Con demo] App2[App de demo] --> Vista2[La vista que estas editando] end style Vista1 fill:#f38ba8,color:#11111b style Vista2 fill:#a6e3a1,color:#11111b style App2 fill:#89b4fa,color:#11111b
Lo que el preview no puede ver
La pregunta razonable es por qué hace falta una demo si ya existe Preview. La respuesta es que son instrumentos distintos que miden cosas distintas, y confundirlos lleva a descubrir tarde una clase entera de fallos.
Un preview es excelente para composición visual: márgenes, tipografías, Dynamic Type, modo oscuro, varios tamaños a la vez. Pero se ejecuta en un entorno recortado que no es el ciclo de vida de una app. Las escenas se comportan de forma aproximada. Los estados de fondo y primer plano no ocurren de verdad. La pila de navegación real, con sus gestos de retroceso y su restauración, no está. El teclado no se comporta igual. Las tareas largas se reinician con cada recompilación del lienzo. Y todo lo que dependa de UIApplication, permisos del sistema o notificaciones simplemente no existe.
Una demo se ejecuta en el simulador y por tanto es una app: puedes rotar el dispositivo, mandarla a segundo plano y traerla de vuelta, matar el proceso, ver la memoria en Instruments, grabar la pantalla para una revisión de diseño y probar VoiceOver de verdad. La regla práctica que emerge es sencilla: el preview para el aspecto, la demo para el comportamiento.
Arranque directo
La demo abre en el estado que investigas, sin login ni navegación previa. El bucle se mide en segundos.
Comportamiento real
Ciclo de vida, escenas, teclado, VoiceOver e Instruments. Nada de eso existe dentro de un preview.
Test de arquitectura
Si la demo enlaza, el módulo no depende de la app. La verificación es del compilador, no de una revisión.
Estados imposibles
El error de red, la lista vacía o el permiso denegado se escriben como valor en vez de provocarse.
Dobles que se escriben una vez
La demo necesita datos falsos, y ahí está el error clásico: fabricarlos dentro del target de demo. Al mes tienes tres versiones incompatibles del mismo usuario de prueba —una en la demo, otra en los preview, otra en los tests— y cada cambio del modelo obliga a arreglar las tres.
Los dobles son un módulo. Se declaran una vez, se comparten y los consumen la demo, los preview y los tests por igual.
// Modulo DominioFalso
import Dominio
extension Usuario {
public static let ejemplo = Usuario(id: .init(1), nombre: "Ada Lovelace")
}
public struct ClienteAPIFalso: ClienteAPI {
public var resultado: Result<Perfil, Error>
public var retardo: Duration
public init(resultado: Result<Perfil, Error>, retardo: Duration = .milliseconds(300)) {
self.resultado = resultado
self.retardo = retardo
}
public func perfil(id: Usuario.ID) async throws -> Perfil {
try await Task.sleep(for: retardo)
return try resultado.get()
}
}
extension ClienteAPIFalso {
public static let conDatos = ClienteAPIFalso(resultado: .success(.ejemplo))
public static let vacio = ClienteAPIFalso(resultado: .success(.sinContenido))
public static let queFalla = ClienteAPIFalso(resultado: .failure(ErrorRed.sinConexion))
public static let lento = ClienteAPIFalso(resultado: .success(.ejemplo), retardo: .seconds(5))
}
Fíjate en el retardo configurable, que es el detalle que la mayoría omite. Un doble que responde instantáneamente hace que los estados de carga sean invisibles, y por tanto que nadie los diseñe ni los pruebe; medio segundo de espera artificial revela los indicadores que faltan, los saltos de layout y los dobles envíos que un usuario con mala cobertura sufre a diario. El doble lento no es un capricho: es la única forma barata de vivir en la red de tus usuarios.
La demo puede ir un paso más allá y ofrecer un selector de escenarios, con lo que se convierte en la herramienta de revisión de diseño y de control de calidad exploratorio del módulo.
enum Escenario: String, CaseIterable, Identifiable {
case conDatos, vacio, queFalla, lento
var id: Self { self }
var cliente: ClienteAPIFalso {
switch self {
case .conDatos: .conDatos
case .vacio: .vacio
case .queFalla: .queFalla
case .lento: .lento
}
}
}
// El .id fuerza a reconstruir la vista al cambiar de escenario
struct SelectorDeEscenario: View {
@State private var escenario: Escenario = .conDatos
var body: some View {
VStack {
Picker("Escenario", selection: $escenario) {
ForEach(Escenario.allCases) { Text($0.rawValue).tag($0) }
}
.pickerStyle(.segmented)
PerfilView(cliente: escenario.cliente, usuario: .ejemplo)
.id(escenario)
}
}
}
Hay una diferencia epistemológica entre creer que un módulo está desacoplado y saberlo, y una app de demo es justamente el aparato que convierte lo primero en lo segundo. La mayoría de las afirmaciones sobre arquitectura de software son inverificables en la práctica: alguien dice que la capa de dominio es independiente de la interfaz, todo el mundo asiente, y nadie lo comprueba porque comprobarlo exigiría un experimento que nadie tiene tiempo de montar. La demo es ese experimento, y su coste marginal, una vez existe el módulo, es de minutos. Si arranca, el módulo funciona sin el resto del sistema: eso no es una interpretación del diagrama, es un hecho observado. Y si deja de arrancar, alguien acaba de introducir un acoplamiento, y lo sabes el día que ocurre en vez de descubrirlo dos años después cuando intentas reutilizar la feature en la app del reloj. La idea tiene ascendencia respetable —es la misma intuición que sostiene el diseño dirigido por tests, donde la dificultad de escribir la prueba es información sobre el diseño y no sobre la prueba— pero la demo la lleva a una escala donde el test unitario no llega, porque un test unitario no ejerce el ciclo de vida de una aplicación, ni el teclado, ni el gesto de retroceso, ni el consumo de memoria. Hay además un efecto de segundo orden que solo se aprecia cuando el equipo lleva meses trabajando así: la demo se convierte en el sitio donde diseño, control de calidad y desarrollo se encuentran, porque es una aplicación que se puede instalar, tocar y romper sin credenciales, sin datos de producción y sin recorrer el flujo entero. Un artefacto de ingeniería que resulta ser también un artefacto de comunicación, y que existe solo porque alguien decidió que la feature tenía que poder vivir sin la app.
Una app de demo es un target mínimo por módulo que arranca la feature aislada con dobles inyectados. Verifica el comportamiento real —ciclo de vida, navegación, teclado, accesibilidad, memoria— que un preview no puede ejercer, y demuestra por construcción que el módulo no depende de la app. Los dobles viven en su propio módulo, se comparten con preview y tests, e incluyen un retardo configurable para que los estados de carga sean visibles.
- Crea un
targetde demo para tu módulo más complejo y consigue que arranque directamente en la pantalla que estás desarrollando. - Extrae los datos falsos de tus
previewa un módulo de dobles y haz que demo,previewy tests consuman los mismos valores. - Añade un doble con retardo de cinco segundos y anota qué estados de carga descubres que faltaban.
- Introduce a propósito una dependencia de la feature hacia el
targetde la app y comprueba cuándo y cómo falla la demo. - Añade un selector de escenarios a la demo y úsalo para una revisión de diseño sin arrancar la app real.