Qué genera la macro @Reducer
La macro @Reducer no es azúcar cosmético sino un generador de código que, en tiempo de compilación, escribe por ti la conformidad al protocolo Reducer, marca el enum de acciones como CasePathable para que la navegación pueda apuntar a sus casos, y decora body con el result builder que permite listar reducers sin pegamento manual. Esta lección disecciona los artefactos exactos que emite la expansión, explica por qué la macro localiza State y Action por nombre y no por anotación, y muestra la variante sobre enum que llega a generar el dominio completo de un destino de navegación. Al terminar sabrás qué código existe en tu binario aunque no aparezca en tu editor.
Hay una asimetría incómoda en el TCA moderno: el código que escribes es breve y el código que se compila no lo es. Entre ambos hay una macro, @Reducer, que en cada compilación genera silenciosamente conformidades, atributos y anotaciones que tú nunca teclearás. Esa generación no es decoración: sin ella no existirían los key paths de caso que la navegación necesita, ni el result builder que permite apilar reducers sin comas, ni la conformidad que convierte tu struct en algo que un Store puede ejecutar. Trabajar con TCA sin saber qué emite la macro es programar contra un código invisible; esta lección lo hace visible pieza por pieza.
- Enumerar los artefactos concretos que la expansión de
@Reducerañade a tu tipo. - Explicar por qué la macro descubre
StateyActionpor convención de nombre y no por anotación explícita. - Distinguir la variante sobre
structde la variante sobreenum, que sintetiza un dominio completo. - Reconocer qué responsabilidades siguen siendo tuyas y cuáles delegó la librería en el compilador.
Lo que desapareció del código fuente
Antes de las macros, conformar el protocolo era un ritual manual. Declarabas el struct, escribías la conformidad, y a menudo repetías los typealias porque la inferencia de tipos asociados fallaba en cuanto había genéricos de por medio. Peor aún: cualquier feature que quisiera participar en navegación tenía que anotar su enum de acciones con @CasePathable a mano, y era un olvido tan frecuente como difícil de diagnosticar.
// La ceremonia previa, escrita íntegramente a mano
struct Contador: Reducer {
struct State: Equatable { var cuenta = 0 }
@CasePathable
enum Action { case incrementar, decrementar }
@ReducerBuilder<State, Action>
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .incrementar: state.cuenta += 1; return .none
case .decrementar: state.cuenta -= 1; return .none
}
}
}
}
Compáralo con la forma actual: una sola línea, @Reducer, sustituye la conformidad explícita, el atributo sobre el enum y el atributo sobre body. La macro no inventa comportamiento nuevo; automatiza tres decisiones que, en la práctica, siempre eran las mismas.
Los artefactos de la expansión
La expansión sobre un struct emite tres cosas, y conviene nombrarlas con precisión porque cada una habilita una capacidad distinta de la librería.
Conformidad al protocolo
Una extensión que declara Contador: Reducer. Es lo que permite que un Store acepte tu tipo y que los operadores de composición lo reciban como valor.
CasePathable sobre Action
El enum de acciones queda anotado con @CasePathable y @dynamicMemberLookup, lo que sintetiza un key path por cada caso y habilita expresiones como \.hijo.
ReducerBuilder sobre body
La propiedad body recibe el atributo @ReducerBuilder parametrizado con tus tipos, y por eso puedes listar varios reducers sin operadores que los unan.
Hay un cuarto efecto, menos visible y más valioso: la macro valida. Si tu tipo no define ni body ni reduce, la expansión falla con un diagnóstico que lo dice; si defines ambos, emite un aviso porque body tiene prioridad y tu reduce quedaría muerto. Esa verificación en tiempo de compilación es la que convierte una clase entera de errores silenciosos en errores tempranos.
flowchart TD A[Codigo fuente con la macro] --> B[Expansion en tiempo de compilacion] B --> C[Extension de conformidad Reducer] B --> D[Atributo CasePathable sobre Action] B --> E[Atributo ReducerBuilder sobre body] B --> F[Diagnosticos de validacion] C --> G[Codigo final compilado] D --> G E --> G
Por qué la macro busca por nombre
Una pregunta legítima: si la macro necesita conocer State y Action para parametrizar el result builder, ¿cómo los encuentra? La respuesta es deliberadamente simple: por nombre. La expansión inspecciona los miembros anidados de tu tipo y toma como estado el que se llame State y como acción el que se llame Action. No hay anotación que los marque, no hay protocolo que los distinga.
@Reducer
struct Perfil {
@ObservableState
struct State: Equatable {
var nombre = ""
var cargando = false
}
enum Action {
case aparecio
case datosRecibidos(String)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .aparecio:
state.cargando = true
return .none
case let .datosRecibidos(nombre):
state.nombre = nombre
state.cargando = false
return .none
}
}
}
}
Esta convención tiene una consecuencia práctica que muerde tarde o temprano: si renombras Action como Acciones para castellanizar tu código, la macro no protesta con un mensaje sobre nombres, sino que falla más abajo con un error sobre tipos asociados que no puede inferir. La lección 5 de este bloque vuelve sobre esa clase de diagnósticos desplazados. Por ahora basta con la regla: los nombres State y Action no son estilísticos, son parte del contrato con la macro.
La variante sobre enum
@Reducer también se aplica a un enum, y ahí deja de ser un ayudante para convertirse en un generador de dominio completo. El caso de uso canónico es el destino de navegación: una pantalla que puede presentar varias features distintas, pero solo una a la vez.
@Reducer
enum Destino {
case detalle(Detalle)
case ajustes(Ajustes)
}
De esas cuatro líneas la macro sintetiza un enum State con un caso por feature que envuelve el estado de cada una, un enum Action simétrico, la conformidad a CaseReducer, y un body que enruta cada acción a su reducer con un Scope por caso. Es la expansión más generosa de toda la librería: escribes la enumeración de destinos posibles y recibes el dominio entero, estado, acciones y enrutamiento incluidos.
El padre consume ese destino como consumiría cualquier hijo opcional, y ahí se ve el ahorro completo: una declaración de presentación, un caso de acción y un operador bastan para que la pantalla pueda mostrar cualquiera de sus destinos con seguridad de tipos.
@Reducer
struct Lista {
@ObservableState
struct State: Equatable {
@Presents var destino: Destino.State?
}
enum Action {
case destino(PresentationAction<Destino.Action>)
case filaPulsada(Int)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
return .none
}
.ifLet(\.$destino, action: \.destino)
}
}
Repara en la última línea: ifLet no lleva closure con un reducer hijo. No hace falta, porque el body estático que la macro generó dentro de Destino ya sabe enrutar hacia cada caso, y el operador lo usa por defecto. Esa omisión es una buena señal de cuánto trabajo ha absorbido la expansión.
Un malentendido frecuente es creer que @Reducer ya se encarga de la observación. No lo hace. @ObservableState sobre el struct State es una macro independiente, de la familia de Observation, que instrumenta los campos para que SwiftUI sepa qué leyó cada vista y vuelva a dibujar solo cuando ese campo cambie. Son dos macros con dominios disjuntos: una construye el reducer, la otra construye la observabilidad del estado.
Lo que de verdad se juega en @Reducer no es ahorro de tecleo, sino la naturaleza del contrato entre tú y la librería. Antes de las macros, TCA solo podía pedirte cosas: pon @CasePathable en tus acciones, anota body con el result builder correcto, no implementes reduce si ya tienes body. Cada una de esas peticiones era una convención sostenida por documentación y por disciplina, y las convenciones sostenidas por disciplina se rompen a escala, siempre, porque la disciplina es un recurso que se agota mientras el código crece. Al mover esas convenciones dentro de una macro, Point-Free las transformó de recomendación en hecho estructural: ya no es que debas anotar el enum, es que el enum está anotado porque el compilador lo anotó, y ninguna prisa, ninguna revisión apresurada ni ningún desarrollador nuevo puede omitirlo. Esa es la operación conceptual profunda de los macros de Swift, y explica por qué TCA los adoptó con tanta agresividad: son el mecanismo por el que una arquitectura deja de depender de que sus usuarios recuerden sus reglas. El precio, y hay precio, es que el código que ejecutas ya no coincide con el que lees, lo que traslada una parte de la comprensión desde la lectura del fuente hacia la lectura de la expansión, que es exactamente el oficio que enseña la lección 4 de este bloque. Aceptar ese trato es entender que en el TCA de hoy tu editor muestra una abreviatura, y que saber leer la versión completa deja de ser curiosidad para volverse competencia.
- Escribe un
@Reducer struct Buscadorcon unStateque tengavar consulta = ""yvar resultados: [String] = [], y unaActioncon los casosconsultaCambiada(String)yresultadosRecibidos([String]). - Antes de mirar nada, escribe en un papel las tres cosas que crees que la macro va a generar. Nombra cada una con el atributo o la conformidad exacta.
- Reescribe la misma feature sin la macro: conformidad explícita,
@CasePathablea mano y@ReducerBuildersobrebody. Comprueba que compila idéntica. - Renombra
ActionaEventoen la versión con macro y anota el mensaje de error literal que produce el compilador. Observa que no menciona el nombre del tipo. - Convierte un par de features en un
@Reducer enum Destinoy enumera cuántas declaraciones te habría costado escribir a mano el estado, las acciones y el enrutamiento que la macro sintetiza.