Swift Macros no Swift 6.2: Guia Completo para Criar e Usar Macros (2026)

Guia prático de Swift Macros no Swift 6.2: como criar macros @attached e @freestanding com SwiftSyntax, testar com assertMacroExpansion e depurar no Xcode 26 com padrões reais de @Observable, @Model e Swift Testing.

Swift Macros no Swift 6.2: Guia Completo 2026

Atualizado: 24 de Maio de 2026

As Swift Macros são um sistema de metaprogramação introduzido no Swift 5.9 e amadurecido no Swift 6.2 que permite gerar código em tempo de compilação a partir de anotações ou expressões, eliminando boilerplate de forma segura, com checagem de tipos e sem custo em runtime. Neste guia você vai aprender a criar macros @attached e @freestanding usando SwiftSyntax, entender os sete papéis (roles) que uma macro pode assumir, e ver como o Xcode 26 facilita o desenvolvimento, depuração e testes, incluindo padrões reais usados por @Observable, @Model e a nova suíte Swift Testing.

  • Macros são plug-ins do compilador escritos em Swift que recebem uma árvore sintática (AST) e devolvem código gerado validado pelo type-checker.
  • Existem dois tipos principais: freestanding (chamadas como #stringify) e attached (anotações como @Observable).
  • Toda macro requer um SwiftCompilerPlugin, uma declaração com @freestanding ou @attached, e implementação via SwiftSyntax 6.0+.
  • Macros são aditivas e herméticas: nunca apagam código existente e rodam em sandbox sem acesso ao sistema de arquivos.
  • O Xcode 26 oferece "Expand Macro" no menu de contexto e suporte nativo a breakpoints no plugin do compilador.
  • Testes de macros usam o pacote swift-syntax/Testing que compara a expansão esperada por string.

O que são Swift Macros e por que usar?

Swift Macros são programas que rodam durante a compilação para transformar ou adicionar código fonte ao seu programa. Diferentemente das macros textuais do C, elas operam sobre a árvore sintática abstrata (AST) usando a biblioteca SwiftSyntax, o que significa que cada token gerado passa pelo type-checker antes de chegar ao binário. Esse design elimina classes inteiras de bugs comuns em metaprogramação baseada em strings ou reflection.

O motivo número um para adotar macros? Remover boilerplate. Antes do Swift 5.9, a observação de propriedades exigia conformidade manual a ObservableObject com @Published em cada atributo. Hoje, @Observable faz tudo em uma anotação.

SwiftData substituiu o Core Data inteiro com a macro @Model, e Swift Testing aposentou o XCTest com @Test e #expect. Em projetos próprios, macros brilham para gerar builders, validadores, parsers JSON tipados, registrar rotas de API e impor invariantes, sempre com a garantia de que o código gerado fica visível via Expand Macro no Xcode.

O custo é zero em runtime: tudo é resolvido em tempo de compilação. O custo em build-time é pequeno (o plugin é compilado uma vez e cacheado), mas existe. Por isso macros valem a pena em padrões repetitivos, não como substituição de funções comuns.

Como configurar um pacote de macros no Swift 6.2

Macros vivem em um Swift Package separado porque o compilador precisa carregá-las como plug-ins. No Xcode 26, escolha File → New → Package → Swift Macro; via linha de comando, use swift package init --type macro. O template gera três targets: a biblioteca pública que declara a macro, o target executável do plugin que a implementa, e um target de testes.

Seu Package.swift deve declarar o tools-version 6.0 ou superior e depender do swift-syntax alinhado com o toolchain do Xcode (atualmente 600.0.0 para Xcode 26):

// swift-tools-version: 6.0
import PackageDescription
import CompilerPluginSupport

let package = Package(
    name: "MinhasMacros",
    platforms: [.macOS(.v13), .iOS(.v17)],
    products: [
        .library(name: "MinhasMacros", targets: ["MinhasMacros"]),
    ],
    dependencies: [
        .package(url: "https://github.com/swiftlang/swift-syntax.git",
                 from: "600.0.0"),
    ],
    targets: [
        .macro(
            name: "MinhasMacrosPlugin",
            dependencies: [
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
            ]
        ),
        .target(name: "MinhasMacros", dependencies: ["MinhasMacrosPlugin"]),
        .testTarget(
            name: "MinhasMacrosTests",
            dependencies: [
                "MinhasMacrosPlugin",
                .product(name: "SwiftSyntaxMacrosTestSupport",
                         package: "swift-syntax"),
            ]
        ),
    ]
)

O target .macro é um tipo especial introduzido no Swift 5.9. O SwiftPM o compila como executável para a plataforma do host (o seu Mac), não para o destino final do app. É por isso que a macro roda em sandbox restrita: sem acesso à rede, sem leitura arbitrária de disco.

Diferença entre macros @freestanding e @attached

Macros se dividem em duas grandes famílias, distinguidas pela sintaxe de invocação:

Característica@freestanding@attached
Invocação#nome(args)@Nome antes da declaração
Onde apareceQualquer expressão ou declaraçãoAntes de class/struct/enum/var/func
Roles disponíveisexpression, declaration, codeItemmember, accessor, peer, extension, memberAttribute
Exemplo familiar#warning, #Predicate, #stringify@Observable, @Model, @Test
Pode adicionar protocolosNãoSim, via role extension
Acessa propriedades existentesNãoSim, lê membros da declaração anotada

Uma macro pode combinar múltiplos roles. A @Observable, por exemplo, é simultaneamente @attached(member) para injetar a propriedade _$observationRegistrar, @attached(memberAttribute) para anotar cada propriedade armazenada com @ObservationTracked, e @attached(extension, conformances: Observable) para adicionar a conformidade ao protocolo. Pensar nas roles como composição é o segredo para entender macros do mundo real.

Como criar sua primeira macro passo a passo

Vamos construir #stringify, a macro canônica do template: ela recebe uma expressão e devolve uma tupla com o valor e a representação textual da expressão. É útil para logging e diagnóstico.

Primeiro, declare a interface pública no target MinhasMacros:

// MinhasMacros/Stringify.swift
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) =
    #externalMacro(module: "MinhasMacrosPlugin", type: "StringifyMacro")

O atributo #externalMacro aponta para o tipo que implementa a expansão dentro do plugin. Agora implemente o plugin:

// MinhasMacrosPlugin/StringifyMacro.swift
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftCompilerPlugin

public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        guard let argument = node.arguments.first?.expression else {
            fatalError("Argumento obrigatório ausente")
        }
        return "(\(argument), \(literal: argument.description))"
    }
}

@main
struct MinhasMacrosPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [StringifyMacro.self]
}

O método expansion retorna um ExprSyntax construído via string interpolation, e a biblioteca SwiftSyntaxBuilder faz o parsing automático. Para usar:

import MinhasMacros

let resultado = #stringify(2 + 3)
print(resultado) // (5, "2 + 3")

No Xcode 26, clique com o botão direito em #stringify(...) e escolha Expand Macro para ver exatamente o que o compilador injetou. Honestamente, esse fluxo de inspeção é essencial. Sempre confira a expansão antes de assumir que a sua macro funciona como você acha que funciona (já perdi tarde fazendo o contrário).

Macros @attached: Member, Accessor, Peer e Extension

O verdadeiro poder das macros aparece em @attached. Vejamos uma macro @AutoInit que gera automaticamente um inicializador a partir das propriedades armazenadas de uma struct. É bem útil em modelos de domínio com muitos campos.

// Declaração pública
@attached(member, names: named(init))
public macro AutoInit() = #externalMacro(
    module: "MinhasMacrosPlugin",
    type: "AutoInitMacro"
)

// Implementação
import SwiftSyntax
import SwiftSyntaxMacros

public struct AutoInitMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        let propriedades = declaration.memberBlock.members.compactMap {
            $0.decl.as(VariableDeclSyntax.self)
        }.flatMap { varDecl -> [(String, String)] in
            varDecl.bindings.compactMap { binding in
                guard
                    let nome = binding.pattern.as(IdentifierPatternSyntax.self)?
                        .identifier.text,
                    let tipo = binding.typeAnnotation?.type.description
                        .trimmingCharacters(in: .whitespaces)
                else { return nil }
                return (nome, tipo)
            }
        }

        let parametros = propriedades
            .map { "\($0.0): \($0.1)" }
            .joined(separator: ", ")
        let atribuicoes = propriedades
            .map { "self.\($0.0) = \($0.0)" }
            .joined(separator: "\n        ")

        let initDecl: DeclSyntax = """
        init(\(raw: parametros)) {
            \(raw: atribuicoes)
        }
        """
        return [initDecl]
    }
}

Uso:

@AutoInit
struct Usuario {
    let id: UUID
    let nome: String
    let email: String
}
// Compilador injeta:
// init(id: UUID, nome: String, email: String) {
//     self.id = id; self.nome = nome; self.email = email
// }

Os outros roles de @attached seguem padrão similar. O accessor injeta getters/setters (útil para tracking de mudanças, como @ObservationTracked); peer cria declarações irmãs à anotada (por exemplo, gerar um EnumName + "Codable"); extension adiciona conformidade a protocolo; e memberAttribute propaga anotações para membros internos. A documentação oficial da Apple sobre macros traz a referência canônica para cada role, vale ter aberta numa aba.

Como testar macros com swift-syntax

Macros são funções puras sobre AST, o que as torna trivialmente testáveis. O pacote SwiftSyntaxMacrosTestSupport oferece a função assertMacroExpansion que compara entrada e saída textuais:

import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import MinhasMacrosPlugin

final class StringifyTests: XCTestCase {
    let macros: [String: Macro.Type] = ["stringify": StringifyMacro.self]

    func testExpansaoSimples() {
        assertMacroExpansion(
            "#stringify(2 + 3)",
            expandedSource: #"(2 + 3, "2 + 3")"#,
            macros: macros
        )
    }

    func testCapturaErroDeArgumento() {
        assertMacroExpansion(
            "#stringify()",
            expandedSource: "#stringify()",
            diagnostics: [
                DiagnosticSpec(
                    message: "Argumento obrigatório ausente",
                    line: 1, column: 1
                )
            ],
            macros: macros
        )
    }
}

Sempre adicione testes para os caminhos de erro. Em produção, emita diagnósticos via context.diagnose(...) em vez de fatalError, para que o desenvolvedor veja a mensagem dentro do Xcode em vez de um crash do compilador (esse foi um bug que eu mesmo introduzi numa macro de validação no ano passado). Para suítes maiores, considere usar a nova Swift Testing com macro @Test também aqui. É meta-circular, mas funciona perfeitamente.

Como depurar macros no Xcode 26

Macros executam em um processo separado (o plugin do compilador), o que dificultava o debugging nas primeiras versões. No Xcode 26, três recursos mudam o jogo: breakpoints diretos no target .macro funcionam quando você roda os testes; a opção Editor → Show Macro Expansion mostra a expansão inline com syntax highlighting; e o comando swift build -Xswiftc -dump-macro-expansions imprime cada expansão no terminal durante o build.

Para inspecionar a AST que você recebe, use print(node.debugDescription) dentro de expansion. A saída aparece em Report Navigator → Build Log. Erros comuns como "Cannot convert value of type 'String' to expected argument type 'ExprSyntax'" quase sempre apontam para falta de raw: em interpolations, e "Macro implementation type not found" significa que o nome do tipo em #externalMacro não bate exatamente com a struct do plugin.

Se a sua macro processa concorrência ou tipos atorados, vale a leitura do nosso guia sobre concorrência acessível no Swift 6.2, já que o type-checker aplica todas as regras de isolamento sobre o código gerado.

Macros no mundo real: @Observable, @Model e Swift Testing

Três macros embarcadas mostram padrões idiomáticos que vale copiar. A @Observable (framework Observation, iOS 17+) substitui ObservableObject com performance superior, porque só notifica observadores das propriedades realmente lidas, eliminando re-renders desnecessários em SwiftUI. Internamente, ela combina três roles (member, memberAttribute, extension) para injetar o registrar, anotar storage e conformar ao protocolo.

A macro @Model do SwiftData vai além. Ela gera código que conversa com o stack de persistência do framework, criando schema, propriedades observáveis e tracking de mudanças sem que o desenvolvedor escreva uma linha de Core Data. Se você ainda não migrou, o guia sobre herança de modelos no SwiftData mostra patterns avançados.

Por fim, @Test e #expect da nova suíte Swift Testing demonstram macros para DSLs: #expect(usuario.idade > 18) expande para código que captura a expressão original, avalia, e em caso de falha imprime "Expectativa falhou: usuario.idade (17) > 18". Esse padrão de capturar AST como string para diagnósticos é, na minha opinião, a aplicação mais elegante de macros @freestanding(expression) em produção.

Armadilhas comuns e boas práticas

Cinco erros recorrentes esgotam horas de debugging. Primeiro: nunca apague código existente. Macros são aditivas por design, e se a sua tentar "transformar" uma declaração removendo membros, ela vai falhar silenciosamente ou produzir comportamento inesperado. Segundo: declare todos os nomes que você vai gerar em names: (named(init), arbitrary, prefixed(...)). Sem isso, o compilador rejeita identificadores não anunciados como medida de higiene.

Terceiro: cuidado com nomes higiênicos. Se a sua macro gera uma variável contador, e o tipo anotado já tem uma contador, há colisão. Use prefixos como _$macroName_contador ou peça ao contexto via context.makeUniqueName("contador").

Quarto: emita diagnósticos amigáveis. Em vez de throw, use context.diagnose(Diagnostic(node: node, message: ...)) para que o erro apareça destacado no Xcode com cor e ícone corretos.

Quinto: versione swift-syntax junto com o toolchain. Uma macro compilada contra swift-syntax 510 não roda no Xcode 26 que usa 600. O CI deve travar versões com .upToNextMajor(from:). Eu peguei esse bug em produção uma vez e foi cansativo, recomendo travar de cara.

Como boa prática final, mantenha cada macro pequena e focada em um único papel mental, mesmo que combine vários roles internamente. Macros que tentam fazer "tudo" viram caixas-pretas impossíveis de manter. Prefira composição: @AutoInit @AutoEquatable @AutoCodable ganha de @AutoEverything, sempre.

Perguntas Frequentes

Macros do Swift são seguras?

Sim. Macros rodam em sandbox isolada sem acesso à rede ou sistema de arquivos arbitrário, o código gerado é validado pelo type-checker do Swift, e a expansão é sempre visível via "Expand Macro" no Xcode. Não há comportamento oculto em runtime.

Qual a diferença entre macro e propertyWrapper?

Property wrappers operam em runtime sobre uma única propriedade e adicionam um wrapper de tipo. Macros operam em tempo de compilação, podem modificar múltiplas declarações ao mesmo tempo, gerar tipos inteiros e conformar a protocolos, sem custo de runtime e sem indireção de tipo.

Posso usar macros em targets que rodam no iOS?

Sim, mas o plugin da macro só compila para a plataforma do host (macOS). O target .macro roda durante o build no seu Mac; o código que ele gera vai para qualquer plataforma suportada pelo seu app, incluindo iOS, watchOS, tvOS e visionOS.

Macros aumentam muito o tempo de build?

O impacto é pequeno e cacheável. O plugin compila uma vez e o resultado é reaproveitado em builds incrementais. Em projetos com centenas de aplicações de macro, o overhead típico fica abaixo de 5 segundos no build completo e é praticamente zero em rebuilds incrementais.

Como migrar código com macro de Swift 5.9 para Swift 6.2?

Atualize o swift-tools-version para 6.0 e a dependência swift-syntax para from: "600.0.0". As APIs de SwiftSyntax mantiveram compatibilidade de fonte na maior parte. Apenas casos que usavam TokenSyntax.text em contextos isolados podem exigir adicionar @MainActor ou nonisolated conforme o novo modelo de concorrência.

Sobre o Autor Priya Raghavan

Priya spent six years at Instacart building the iOS shopper app, where she led the migration from UIKit to SwiftUI across 80+ screens and cut crash-free sessions from 99.2% to 99.87%. Before that, she was a contractor at a Bay Area design studio shipping App Store apps for two Fortune 500 retail clients. She focuses on practical SwiftUI architecture - what holds up when you have 12 engineers committing to the same codebase, not just toy MVVM examples. Her recent work involves The Composable Architecture, Swift concurrency migration audits, and reducing main-thread hangs on older devices like the iPhone XR that enterprise fleets still ship. Priya runs a small consultancy in Oakland and occasionally speaks at try! Swift NYC. She has been writing Swift since the Objective-C bridging days of 2015.