Al final de la parte sobre property wrappers dejamos una pista: «no todo lo que lleva arroba es un wrapper». @Observable es el protagonista.
Este contenido continúa a partir del artículo anterior Swift avanzado #6.
Parece un wrapper, pero en realidad es un macro. Al usar #Preview en Xcode o @Model de SwiftData, ya somos consumidores de macros.
En esta entrega de la serie avanzada resumimos qué son los macros introducidos en Swift 5.9, qué problemas resuelven y qué coste tienen. SE-0382: Expression Macros
El problema que resuelven los macros — el último bastión del código repetitivo
Swift ha añadido continuamente mecanismos para reducir el código repetitivo: implementaciones predeterminadas de protocolos, síntesis automática de Codable y property wrappers.
Pero quedaba un área que estas herramientas no podían tocar: leer la estructura de un tipo y generar código acorde con ella.
Observation es un buen ejemplo. Para avisar a los observadores cuando cambia una propiedad, se necesita código de seguimiento para cada propiedad almacenada.
Con un wrapper(@Published), había que añadir una arroba a cada propiedad; las que se omitían quedaban fuera de la observación sin aviso.
Lo necesario es: «genera código adaptado a cada nombre para todas las propiedades almacenadas de esta clase». Eso exige leer la estructura del tipo, algo que está fuera de las capacidades de un wrapper.
Tradicionalmente, esta área quedaba en manos de dos opciones: las técnicas dinámicas del runtime de Objective-C (coste en tiempo de ejecución y opacidad), o herramientas externas de generación como Sourcery (gestión separada fuera del pipeline de compilación).
Los macros incorporan esta tarea al lenguaje y, además, al tiempo de compilación.
Cómo funcionan — plugins de generación de código conectados al compilador
Los macros de Swift son plugins del compilador. Su naturaleza es fundamentalmente distinta de la sustitución de texto del preprocesador de C. El proceso es el siguiente.
- Cuando el compilador encuentra un uso de macro (# o @) en el código fuente, pasa el árbol sintáctico (AST) de ese código a la implementación del macro.
- La implementación del macro es un programa Swift que se ejecuta en un proceso separado. Analiza el árbol con la biblioteca SwiftSyntax y devuelve nuevos fragmentos de código.
- El compilador integra el resultado en su ubicación original y después continúa con una compilación normal.
De esta estructura se derivan varias propiedades importantes.
Primero, el resultado es código Swift real que se puede inspeccionar. En Xcode, haz clic derecho en el macro y selecciona «Expand Macro» para ver directamente el código generado.
Poder retirar en cualquier momento la cortina de la magia es la versión de los macros de «ocultar, pero no esconder» que vimos en la parte sobre Progressive Disclosure.
Segundo, es higiénico. Las variables creadas por el macro se gestionan para no entrar en conflicto con los nombres del código circundante, y el macro no puede modificar código fuera del ámbito de su rol declarado.
Los efectos secundarios infames de los macros de C quedan bloqueados desde la fase de diseño.
Tercero, está aislado en un sandbox. El proceso del macro no puede acceder a archivos ni a la red, por lo que no se convierte en un agujero de seguridad que ejecute código arbitrario durante la compilación.
Dos ramas — freestanding(#) y attached(@)
Los macros se dividen en dos ramas según cómo se adjuntan.
El macro freestanding(#) genera código en ese lugar. #Preview { MyView() }crea la estructura de registro de la vista previa.
Los macros del tipo #URL("https://apple.com") validan cadenas durante la compilación y crean una URL desempaquetada. Son macros que ocupan el lugar de una expresión.
El macro attached(@) amplía la declaración a la que está unido. Sus roles están subdivididos.
Existen member, que añade miembros; accessor, que agrega accesores a propiedades; y extension, que añade conformidad a protocolos. Un mismo macro puede asumir varios roles.
@Observable combina exactamente estas funciones. Añade accesores de seguimiento a cada propiedad almacenada de la clase (accessor) y miembros del registro de observación (member). Además, añade la conformidad al protocolo Observable (extension).
Por eso la incomodidad de la era de @Published, que exigía una arroba por propiedad, se redujo a una sola arroba en el tipo.
Con esta distinción se entienden mejor las documentaciones de las bibliotecas. Piensa en @Model de SwiftData, #expect del framework de pruebas (el macro visto en la parte sobre Swift Testing) y @Reducer de Composable Architecture.
Las API más importantes del ecosistema reciente pertenecen a una de estas dos ramas.
La factura — ¿crear macros o limitarse a usarlos?
Los macros tienen un coste claro, y el cálculo es distinto para consumidores y productores.
Para quien los usa, el coste principal suele ser el tiempo de compilación. La implementación del macro depende de SwiftSyntax, que es una biblioteca grande.
Por eso, al compilar por primera vez un paquete que usa macros, se incluye la compilación completa de SwiftSyntax.
Es habitual que una compilación limpia en CI pase a durar varios minutos, y la comunidad sigue perfeccionando mitigaciones como distribuir binarios precompilados.
Aun así, para el consumidor suele ser un coste asumible. Se puede inspeccionar con Expand Macro y no hay coste en tiempo de ejecución.
Para quien los crea, el coste es mucho mayor. Escribir macros significa «escribir código que manipula código», así que la dificultad sube un nivel.
Hay que aprender la API de SwiftSyntax, crear pruebas específicas para macros (comparar código de entrada con código de salida esperado) y diseñar mensajes de diagnóstico.
Por eso, en la práctica conviene ser conservador. Si el problema se resuelve con implementaciones predeterminadas de protocolos, genéricos o wrappers, esas opciones deben ir primero.
Los macros propios solo son candidatos cuando existe ampliamente en el código del equipo una «repetición que exige leer la estructura del tipo». Es la versión macro de YAGNI (You Aren’t Gonna Need It: el principio de no construir algo hasta que sea necesario).
Para la mayoría de los equipos, un macro no es una herramienta para crear, sino para entender y usar correctamente lo que ofrece el framework.
Resumen
- Un macro es un plugin del compilador que recibe un árbol sintáctico en tiempo de compilación y genera código (Swift 5.9). A diferencia de la sustitución de texto de los macros de C, lee la estructura de los tipos y se ejecuta de forma higiénica y aislada.
- freestanding(#) genera código en ese lugar (#Preview), mientras attached(@) amplía la declaración a la que está unido (@Observable, @Model).
- El resultado generado es código Swift real que se puede inspeccionar en cualquier momento con Expand Macro. No todo lo que lleva una arroba es un wrapper; a partir de ahora, distingue la palabra macro al leer la documentación.
- El coste es el tiempo de compilación derivado de SwiftSyntax (consumidor) y la alta dificultad de creación (productor). Crea macros propios solo cuando se confirme una «repetición generalizada que exige leer la estructura del tipo». SE-0389: Attached Macros
La próxima entrega trata conceptos que Swift tomó del territorio de Rust: los tipos no copiables ~Copyable y el mundo de la propiedad que abren borrowing y consuming.
Fuentes y criterios de verificación
- SE-0389: Attached Macros — Swift Evolution · texto original del estándar y la especificación · verificado el 2026-08-17 · fundamento: roles de attached macro y declaraciones que puede generar
- SE-0382: Expression Macros — Swift Evolution · texto original del estándar y la especificación · verificado el 2026-08-17 · fundamento: modelo de expression macro de Swift 5.9 y plugins del compilador

![Imagen de portada de [Swift avanzado #7] Macros de Swift: guía de @Observable](/assets/images/posts/2c0c42f3-4f02-48e1-8e36-2d9f4f1bf9b6/swift-macros-compiler-plugin.jpg)