Por qué TCA en el mundo Apple
Los cuatro niveles previos diseccionaron TCA por dentro; este cierra el nivel con la pregunta que decide si algo de eso te concierne: por qué elegirla en el mundo Apple y cuándo no. Parte de un hecho incómodo —SwiftUI es un motor de vistas excelente y una arquitectura inexistente— y nombra los tres dolores que su silencio produce a escala: el estado disperso en múltiples `@State`, el efecto asíncrono incrustado en el `body` de la vista, y la lógica que solo se prueba montando la interfaz. Frente a ellos, TCA opone un vocabulario fijo que da consistencia y composición en apps grandes, y un mismo modelo que corre en iOS, macOS, iPadOS, visionOS, tvOS y watchOS. Pero no lo vende sin factura: mide su coste real —ceremonia y curva— y traza la frontera fina pero nítida donde `@Observable` a secas basta y TCA sobra, sustituyendo el reflejo por un criterio de tamaño.
Los cuatro niveles anteriores diseccionaron TCA por dentro: sus piezas, su herencia y su flujo. Queda la pregunta que decide si algo de esto te concierne —por qué elegirla en el mundo Apple, y cuándo no—. La respuesta empieza por un hecho incómodo: SwiftUI es un motor de vistas excelente y una arquitectura inexistente. Te dice cómo se dibuja el estado, pero calla dónde vive, cómo se muta con orden y cómo se prueba sin arrancar la interfaz. En una pantalla suelta ese silencio es libertad; en una app de cientos de pantallas y varios equipos es deuda. TCA no compite con SwiftUI: rellena el hueco que SwiftUI deja a propósito. Esta lección cierra el nivel poniendo el modelo frente al problema real que resuelve, midiendo su coste sin adornos y trazando la frontera —fina pero nítida— donde @Observable a secas basta y TCA sobra.
- Nombrar los tres dolores del SwiftUI por defecto a escala: estado disperso en
@State, efectos incrustados en la vista y lógica que solo se prueba montando la UI. - Explicar por qué un vocabulario único —
State,Action,Reducer,Effect— da consistencia y composición en apps grandes con varios equipos. - Justificar la portabilidad de un mismo modelo a iOS, macOS, iPadOS, visionOS, tvOS y watchOS.
- Fijar el coste de TCA —ceremonia y curva— y el criterio para no usarla frente a
@Observablepuro.
Los tres dolores del SwiftUI a escala
El SwiftUI idiomático de una pantalla pequeña es un placer, y por eso engaña. La ergonomía que hace delicioso un formulario de tres campos es la misma que, multiplicada por una app real, produce tres patologías reconocibles. La primera es el estado disperso: cada vista guarda sus @State privados, y una feature con diez datos que dependen entre sí termina con esos diez repartidos por media docena de vistas y bindings, sin un lugar donde leer qué puede contener la feature. La segunda es el efecto incrustado: la llamada de red, el temporizador o el acceso a disco viven dentro de un .task o un .onAppear, mezclados con el código que dibuja, de modo que la lógica de negocio queda soldada a la vista que la hospeda. La tercera es consecuencia de las dos anteriores: para probar esa lógica hay que montar la interfaz entera, porque no existe fuera de ella.
// SwiftUI por defecto: estado disperso y efecto soldado a la vista
struct PerfilView: View {
@State private var nombre = ""
@State private var cargando = false
@State private var error: String?
@State private var usuario: Usuario?
var body: some View {
Form {
TextField("Nombre", text: $nombre)
if cargando { ProgressView() }
}
.task {
cargando = true
do { usuario = try await api.cargarPerfil() } // efecto dentro del body
catch { error = "fallo al cargar" }
cargando = false
}
}
}
Nada de esto está mal para una pantalla; está mal para mil. Cuatro @State sueltos ya obligan a rastrear a mano qué los muta; el efecto dentro del .task no se puede ejecutar sin una vista viva, así que probar el camino de error exige simular una interfaz; y cuando dos pantallas lejanas necesiten el mismo usuario, no habrá dónde ponerlo salvo subirlo a mano por la jerarquía. TCA no inventa un problema para venderte la cura: da forma explícita a un desorden que, por defecto, SwiftUI te deja gestionar con disciplina propia o sin ninguna.
La respuesta de TCA: un vocabulario que escala
Frente a esos tres dolores, TCA no ofrece más potencia sino una forma fija. Todo el estado de la feature se reúne en un valor, State; todo lo que puede ocurrir es un caso de Action; toda mutación pasa por el Reducer; y todo trato con el mundo exterior se devuelve como un Effect en vez de ejecutarse en la vista. Ese vocabulario —cuatro nombres y un Store que los orquesta— es idéntico en cada feature de la app, y ahí está su valor a escala: quien abre una feature que nunca ha visto ya sabe dónde mirar, porque la forma no cambia. La consistencia deja de depender de la disciplina individual y pasa a estar impuesta por los tipos.
Estado reunido
Todo el estado de la feature vive en un State, no repartido en @State de varias vistas. Hay un único sitio donde leer qué puede contener la feature.
Composición explícita
Las features se combinan como valores: un reducer padre integra los hijos y un store se parte con scope. Las apps grandes se ensamblan a partir de piezas independientes.
Prueba sin UI
Como la lógica vive en un reducer puro, TestStore la ejercita sin montar una sola vista, afirmando cada cambio de estado paso a paso.
Un modelo, seis SO
El mismo reducer corre en iOS, macOS, iPadOS, visionOS, tvOS y watchOS; solo cambia la vista conectada al Store.
Esa forma fija es también lo que habilita la composición, el segundo gran argumento a escala. En SwiftUI por defecto, unir dos pantallas significa cablear a mano sus estados y sus callbacks; en TCA cada feature es un valor autocontenido —su State, su Action, su Reducer— y una feature mayor se arma integrando las menores como piezas independientes que no se conocen entre sí. Un reducer padre embebe a sus hijos y reparte las acciones; el Store se divide con scope para que cada vista reciba solo su rebanada. Así, una app de cincuenta pantallas deja de ser un monolito de estado y se vuelve un árbol de features que se prueban y se razonan por separado y se ensamblan sin fricción.
De las cuatro, la tercera es la que más distancia a TCA del SwiftUI por defecto, porque convierte lo intestable en trivial de probar. Como el reducer es una función pura y los efectos son valores con sus dependencias inyectadas, TestStore reproduce una feature entera en memoria y exige declarar, acción por acción, exactamente cómo cambia el estado; si algo cambia de más o de menos, la prueba falla.
@Test
func cargaDePerfil() async {
let store = TestStore(initialState: Perfil.State()) {
Perfil()
} withDependencies: {
$0.api.cargarPerfil = { Usuario(nombre: "Ada") }
}
await store.send(.alAparecer) { $0.cargando = true }
await store.receive(\.perfilCargado) {
$0.cargando = false
$0.usuario = Usuario(nombre: "Ada")
}
}
Puedes probar una vista SwiftUI con pruebas de UI, pero son lentas, frágiles y aproximadas. Lo que TestStore añade es exhaustividad: obliga a afirmar el estado completo tras cada acción y falla si olvidas uno, de modo que la prueba documenta el comportamiento entero de la feature y se rompe en cuanto la lógica cambia sin querer. Esa garantía nace del reducer puro y las dependencias inyectadas; sin esas dos restricciones, no existe.
Seis plataformas, un solo modelo
El argumento de portabilidad se enuncia rápido y se subestima despacio. Como el reducer no importa SwiftUI ni UIKit —solo manipula valores— la lógica de una feature es independiente de la plataforma que la muestre. La misma feature de autenticación, de carrito o de ajustes se comparte entre el iPhone, el Mac, el Apple Watch y el Vision Pro; lo único que se reescribe es la vista que se enchufa al Store. Para una organización que mantiene la misma app en las seis plataformas de Apple, eso significa una sola fuente de verdad para el comportamiento y seis capas de presentación delgadas, en vez de seis reimplementaciones que divergen con el tiempo.
flowchart TD CORE[State Action Reducer Effect] --> STORE[Store runtime] STORE --> IOS[vista iOS y iPadOS] STORE --> MAC[vista macOS] STORE --> VIS[vista visionOS] STORE --> WT[vista watchOS y tvOS] style CORE fill:#cba6f7,color:#11111b style STORE fill:#89b4fa,color:#11111b style IOS fill:#a6e3a1,color:#11111b style MAC fill:#a6e3a1,color:#11111b style VIS fill:#a6e3a1,color:#11111b style WT fill:#a6e3a1,color:#11111b
Esa ortogonalidad entre lógica y plataforma es difícil de conseguir con el modelo por defecto, donde la lógica tiende a filtrarse en el body de vistas específicas de cada dispositivo. No es una promesa de marketing: es la consecuencia mecánica de haber sacado el comportamiento de la vista y haberlo encerrado en un valor puro que ninguna capa de presentación puede contaminar.
El coste real y cuándo NO usar TCA
Sería deshonesto vender todo esto sin su factura. TCA cuesta, y su coste tiene dos partidas. La primera es ceremonia: para incrementar un contador declaras un State, un caso de Action, un Reducer y conectas un Store —donde @State private var cuenta = 0 habría bastado—. La segunda es curva: macros como @Reducer y @ObservableState, el manejo de Effect, la inyección de dependencias y TestStore forman un cuerpo conceptual que un equipo tarda semanas en interiorizar. Ninguna de las dos desaparece con la práctica; se amortizan, que es distinto.
Y como todo coste fijo, solo compensa cuando el beneficio lo supera. Para una app pequeña, un prototipo o una feature con cuatro banderas de UI, el SwiftUI moderno con @Observable ya ofrece un modelo limpio y testeable sin ninguna ceremonia.
// A veces esto basta y TCA sobra: un @Observable directo
@Observable final class ContadorModelo {
private(set) var cuenta = 0
func incrementar() { cuenta += 1 }
func decrementar() { cuenta -= 1 }
}
Ese modelo es observable, se prueba llamando a sus métodos y no arrastra ninguna librería. La regla, entonces, es simétrica a la que este track repitió con Redux: el flujo unidireccional, el estado explícito y la testabilidad exhaustiva de TCA son baratos frente a un problema grande —muchas features, varios equipos, efectos que orquestar, seis plataformas— y caros frente a uno pequeño. Adoptar TCA en una app de tres pantallas no te da orden; te da ceremonia sin cobrar su beneficio.
El fallo típico no es técnico sino cultural: elegir TCA porque es lo que usa el equipo admirado, o rechazarla por reflejo antibiblioteca. Ambos deciden por identidad y no por análisis. Si tu app es pequeña y estable, @Observable puro es la respuesta correcta y TCA una losa. Si es grande, longeva y compartida entre plataformas y equipos, la ceremonia que hoy te pesa es justo lo que mañana te salva. El tamaño del problema, no la moda, dicta la herramienta.
La forma madura de situar TCA en el mundo Apple es dejar de verla como una alternativa a SwiftUI y entenderla como su complemento en un régimen concreto de escala. SwiftUI resolvió magistralmente el problema de la vista —declararla como función del estado— pero dejó abierto, y a propósito, el problema de la arquitectura: dónde vive ese estado, cómo se muta con orden, cómo se aísla el efecto y cómo se prueba el comportamiento sin arrancar la interfaz. En una pantalla suelta ese hueco no duele, porque el desorden todavía cabe en una cabeza; en una app de cientos de pantallas, varios equipos y seis plataformas, ese hueco es exactamente donde nacen los bugs irreproducibles, las fuentes de verdad duplicadas y la lógica que nadie se atreve a tocar porque no se puede probar. TCA rellena ese hueco con la misma apuesta que Elm y Redux hicieron antes: cambiar potencia por restricción, y cobrar por adelantado una ceremonia que solo se amortiza con el tamaño. Por eso la pregunta correcta nunca es si TCA es mejor que SwiftUI —no compiten— sino si tu problema ya es lo bastante grande como para que su factura fija quede por debajo del caos que evita. El ingeniero que ha recorrido este nivel no sale siendo pro-TCA ni anti-TCA; sale sabiendo medir tres cosas —cuántas features tiene su app, cuántas manos la tocan y cuántas plataformas la muestran— y eligiendo @Observable puro cuando esas medidas son pequeñas y TCA cuando son grandes. Toda la madurez que este tema exige cabe en no confundir la herramienta con la identidad, y en recordar que la restricción que hoy parece burocracia es, en las apps que la merecen, la única forma conocida de que un sistema enorme siga siendo razonable de leer, reproducir y probar años después de escrito.
- Toma una feature real de una app SwiftUI tuya y cuenta cuántos
@State,@Bindingy modelos participan en ella. Si superan lo que puedes recordar de un vistazo, el primer dolor te aplica. - Localiza un efecto asíncrono dentro de un
.tasko un.onAppeary pregúntate cómo probarías su camino de error sin montar la vista. Si la respuesta es que no puedes, el tercer dolor te aplica. - Reescribe esa feature en las cuatro piezas —
State,Action,Reducer,Effect— al menos sobre papel, y estima cuántas líneas de ceremonia añade frente al beneficio en legibilidad. - Cuenta cuántas plataformas de Apple mantiene tu app y cuántos equipos tocan el mismo estado. Cuanto más altos esos números, más se amortiza la factura fija de TCA.
- Escribe la versión
@Observablepura de la misma feature y compárala honestamente. Elige la herramienta por el tamaño del problema, no por la que preferirías usar.