Swift Macros: exemplos práticos de metaprogramação
Guia completo sobre Swift Macros: criação de macros freestanding e attached, manipulação da AST com swift-syntax e exemplos práticos para eliminar código repetitivo.

As Swift Macros, introduzidas com o Swift 5.9 e o Xcode 15, representam uma revolução na forma de escrever código Swift. Esse recurso permite gerar código em tempo de compilação, eliminando boilerplate sem abrir mão da segurança de tipos estática. Diferentemente das macros do pré-processador C, as Swift Macros são seguras em tipos, integradas ao compilador e totalmente suportadas pelas ferramentas de desenvolvimento.
Este guia explora a criação de Swift Macros do início ao fim: dos conceitos fundamentais às implementações avançadas, com exemplos de código funcionais prontos para adaptar a qualquer projeto iOS.
Entendendo os tipos de Swift Macros
O Swift oferece duas categorias principais de macros, cada uma com casos de uso distintos. As macros freestanding atuam de forma autônoma como expressões ou declarações, enquanto as macros attached se associam a declarações existentes para modificá-las ou enriquecê-las.
Macros freestanding: expressão e declaração
Macros freestanding começam com o símbolo # e podem retornar um valor (expressão) ou criar novas declarações. A seguir, um exemplo concreto de macro de expressão:
// Freestanding expression macro - generates a value
let buildInfo = #buildDate
// Expansion → "2026-03-11 10:30:45"
// Freestanding macro with arguments
let message = #stringify(1 + 2)
// Expansion → "1 + 2 = 3"
// Freestanding declaration macro - creates declarations
#makeCase("success", "failure", "pending")
// Expansion →
// case success
// case failure
// case pendingA diferença essencial entre expressão e declaração está no resultado: uma expressão produz um valor, ao passo que uma declaração produz código estrutural (tipos, funções, variáveis).
Macros attached: os cinco papéis
Macros attached começam com @ e ficam à frente de uma declaração. O Swift define cinco papéis distintos para essas macros:
// @attached(peer) - adds declarations at the same level
@AddAsync
func fetchUser(id: Int) -> User { ... }
// Expansion → adds func fetchUserAsync(id: Int) async -> User
// @attached(accessor) - adds getters/setters
@UserDefault("theme")
var currentTheme: String
// Expansion → adds get { UserDefaults.standard.string(...) }
// @attached(member) - adds members to a type
@AutoEquatable
struct Point {
var x: Int
var y: Int
}
// Expansion → adds static func == (lhs: Point, rhs: Point) -> Bool
// @attached(memberAttribute) - applies attributes to members
@CodableKeys
struct Config {
var apiUrl: String
var timeout: Int
}
// Expansion → adds @CodingKey("api_url") before apiUrl
// @attached(conformance) / @attached(extension) - adds conformances
@Hashable
struct User {
var id: Int
var name: String
}
// Expansion → adds extension User: Hashable { ... }Esses papéis podem ser combinados para criar macros poderosas, capazes de transformar o código em várias dimensões ao mesmo tempo.
Configuração do projeto para criar macros
A criação de Swift Macros exige um Swift Package com uma estrutura específica. O pacote depende do swift-syntax, biblioteca oficial para manipular código Swift como uma árvore de sintaxe abstrata (AST).
Estrutura do Package.swift
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyMacros",
platforms: [.macOS(.v10_15), .iOS(.v13)],
products: [
// Library exposing macros to the main project
.library(
name: "MyMacros",
targets: ["MyMacros"]
),
// Executable for testing macros
.executable(
name: "MyMacrosClient",
targets: ["MyMacrosClient"]
)
],
dependencies: [
// Required dependency for macros
.package(
url: "https://github.com/apple/swift-syntax.git",
from: "509.0.0"
)
],
targets: [
// Compiler plugin containing implementation
.macro(
name: "MyMacrosPlugin",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
// Target exposing macro declarations
.target(
name: "MyMacros",
dependencies: ["MyMacrosPlugin"]
),
// Test client
.executableTarget(
name: "MyMacrosClient",
dependencies: ["MyMacros"]
),
// Unit tests
.testTarget(
name: "MyMacrosTests",
dependencies: [
"MyMacrosPlugin",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
]
)
]
)Essa configuração separa de forma clara as declarações das macros (o que o código cliente enxerga) de sua implementação (executada em tempo de compilação).
São necessários no mínimo três arquivos: MyMacros.swift para as declarações, MyMacrosPlugin.swift para as implementações e MyMacrosTests.swift para os testes. Essa separação facilita a manutenção.
Criando uma macro de expressão
Macros de expressão geram um valor utilizável diretamente no código. Veja como criar uma macro #unwrap que faz o unwrap de um opcional com uma mensagem de erro personalizada que inclui o nome da variável.
Declaração da macro
import Foundation
/// Macro that unwraps an optional with an explicit error message
/// Usage: let value = #unwrap(optionalValue)
/// Expansion: guard let optionalValue else { fatalError("...") }; optionalValue
@freestanding(expression)
public macro unwrap<T>(_ value: T?) -> T = #externalMacro(
module: "MyMacrosPlugin",
type: "UnwrapMacro"
)A assinatura indica que a macro recebe um opcional e devolve o valor não opcional. O #externalMacro aponta para a implementação dentro do plugin.
Implementação com swift-syntax
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftCompilerPlugin
public struct UnwrapMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
// Get the first argument passed to the macro
guard let argument = node.argumentList.first?.expression else {
throw MacroError.missingArgument
}
// Extract the variable name for the error message
let variableName = argument.description.trimmingCharacters(
in: .whitespacesAndNewlines
)
// Generate the expansion code
// Uses an immediately-invoked closure to encapsulate the guard
return """
{
guard let value = \(argument) else {
fatalError("Failed to unwrap '\\(\(literal: variableName))' - value was nil")
}
return value
}()
"""
}
}
// Custom errors for macros
enum MacroError: Error, CustomStringConvertible {
case missingArgument
case invalidSyntax(String)
var description: String {
switch self {
case .missingArgument:
return "The macro requires an argument"
case .invalidSyntax(let message):
return "Invalid syntax: \(message)"
}
}
}O método expansion recebe o nó da AST que representa a chamada da macro e o contexto de compilação. Ele retorna um ExprSyntax que contém o código gerado.
Registro do plugin
import SwiftCompilerPlugin
import SwiftSyntaxMacros
@main
struct MyMacrosPlugin: CompilerPlugin {
// List all macros provided by this plugin
let providingMacros: [Macro.Type] = [
UnwrapMacro.self,
// Add other macros here
]
}Esse ponto de entrada informa ao compilador as macros disponíveis no plugin.
Pronto para mandar bem nas entrevistas de iOS?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Criando uma macro attached do tipo member
Macros member adicionam membros (propriedades, métodos, tipos aninhados) a um tipo existente. A seguir, uma macro @AutoInit que gera automaticamente um inicializador com todas as propriedades armazenadas.
Declaração e implementação completas
/// Automatically generates an initializer with all stored properties
@attached(member, names: named(init))
public macro AutoInit() = #externalMacro(
module: "MyMacrosPlugin",
type: "AutoInitMacro"
)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] {
// Verify the macro is applied to a struct or class
guard declaration.is(StructDeclSyntax.self) ||
declaration.is(ClassDeclSyntax.self) else {
throw MacroError.invalidSyntax(
"@AutoInit can only be applied to structs and classes"
)
}
// Collect stored properties
let properties = declaration.memberBlock.members
.compactMap { $0.decl.as(VariableDeclSyntax.self) }
.filter { isStoredProperty($0) }
// Generate initializer parameters
let parameters = properties.compactMap { property -> String? in
guard let binding = property.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
let type = binding.typeAnnotation?.type else {
return nil
}
let name = identifier.identifier.text
let typeName = type.description.trimmingCharacters(in: .whitespaces)
// Check if the property has a default value
if binding.initializer != nil {
return "\(name): \(typeName) = \(binding.initializer!.value)"
}
return "\(name): \(typeName)"
}
// Generate assignments in the init body
let assignments = properties.compactMap { property -> String? in
guard let binding = property.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else {
return nil
}
let name = identifier.identifier.text
return "self.\(name) = \(name)"
}
// Build the complete initializer
let initDecl: DeclSyntax = """
public init(\(raw: parameters.joined(separator: ", "))) {
\(raw: assignments.joined(separator: "\n "))
}
"""
return [initDecl]
}
// Check if a variable is a stored property (not computed)
private static func isStoredProperty(_ variable: VariableDeclSyntax) -> Bool {
guard let binding = variable.bindings.first else { return false }
// A computed property has an accessor block with get/set
if let accessor = binding.accessorBlock {
// If it's a block with explicit accessors, it's computed
if accessor.accessors.is(AccessorDeclListSyntax.self) {
return false
}
}
// let or var without accessor = stored property
return true
}
}Uso da macro AutoInit
@AutoInit
struct User {
let id: UUID
var name: String
var email: String
var isActive: Bool = true
}
// Automatically generated code:
// public init(id: UUID, name: String, email: String, isActive: Bool = true) {
// self.id = id
// self.name = name
// self.email = email
// self.isActive = isActive
// }
// Usage
let user = User(id: UUID(), name: "Alice", email: "alice@example.com")
// isActive uses the default valueEssa macro elimina o boilerplate do inicializador, algo especialmente útil em modelos de dados com muitas propriedades.
Macro attached peer para geração async
Macros peer adicionam declarações no mesmo nível da declaração anotada. Veja a macro @AddAsync que gera a versão async de uma função baseada em completion handler.
/// Automatically generates an async version of a function with completion handler
@attached(peer, names: suffixed(Async))
public macro AddAsync() = #externalMacro(
module: "MyMacrosPlugin",
type: "AddAsyncMacro"
)import SwiftSyntax
import SwiftSyntaxMacros
public struct AddAsyncMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// Verify it's a function
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
throw MacroError.invalidSyntax(
"@AddAsync requires a function"
)
}
let functionName = funcDecl.name.text
let asyncFunctionName = "\(functionName)Async"
// Analyze parameters to find the completion handler
let parameters = funcDecl.signature.parameterClause.parameters
// Filter parameters (exclude completion handler)
var regularParams: [String] = []
var completionType: String? = nil
for param in parameters {
let paramType = param.type.description
// Detect a completion handler (closure with Result or simple value)
if paramType.contains("->") && paramType.contains("Void") {
// Extract the return type from completion
completionType = extractCompletionReturnType(from: paramType)
} else {
let paramName = param.firstName.text
let paramSecondName = param.secondName?.text
let label = paramSecondName ?? paramName
regularParams.append("\(paramName): \(paramType)")
}
}
guard let returnType = completionType else {
throw MacroError.invalidSyntax(
"No completion handler found"
)
}
// Generate arguments for internal call
let callArgs = parameters.dropLast().map { param in
let name = param.firstName.text
return "\(name): \(name)"
}.joined(separator: ", ")
// Generate the async function
let asyncFunc: DeclSyntax = """
func \(raw: asyncFunctionName)(\(raw: regularParams.joined(separator: ", "))) async throws -> \(raw: returnType) {
try await withCheckedThrowingContinuation { continuation in
\(raw: functionName)(\(raw: callArgs.isEmpty ? "" : callArgs + ", ")completion: { result in
switch result {
case .success(let value):
continuation.resume(returning: value)
case .failure(let error):
continuation.resume(throwing: error)
}
})
}
}
"""
return [asyncFunc]
}
// Extract return type from a Result type
private static func extractCompletionReturnType(from type: String) -> String {
// Simplified pattern - in production, use the AST
if let match = type.range(of: #"Result<([^,]+)"#, options: .regularExpression) {
var result = String(type[match])
result = result.replacingOccurrences(of: "Result<", with: "")
return result.trimmingCharacters(in: .whitespaces)
}
return "Void"
}
}Demonstração da macro AddAsync
class NetworkService {
@AddAsync
func fetchUser(
id: Int,
completion: @escaping (Result<User, Error>) -> Void
) {
// Implementation with callback
URLSession.shared.dataTask(with: URL(string: "/users/\(id)")!) { data, _, error in
if let error = error {
completion(.failure(error))
} else if let data = data {
let user = try? JSONDecoder().decode(User.self, from: data)
completion(.success(user!))
}
}.resume()
}
// Automatically generates:
// func fetchUserAsync(id: Int) async throws -> User {
// try await withCheckedThrowingContinuation { continuation in
// fetchUser(id: id, completion: { result in
// switch result {
// case .success(let value):
// continuation.resume(returning: value)
// case .failure(let error):
// continuation.resume(throwing: error)
// }
// })
// }
// }
}
// Modern usage with async/await
let user = try await networkService.fetchUserAsync(id: 42)O nome da função gerada precisa ser declarado em names: do atributo @attached. Aqui, suffixed(Async) indica que a função gerada vai ter o sufixo "Async" anexado ao nome original.
Testes unitários para macros
Testar macros é essencial, já que elas geram código que será compilado. O Swift fornece SwiftSyntaxMacrosTestSupport para facilitar esses testes.
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import MyMacrosPlugin
final class MyMacrosTests: XCTestCase {
// Dictionary of macros to test
let testMacros: [String: Macro.Type] = [
"unwrap": UnwrapMacro.self,
"AutoInit": AutoInitMacro.self,
"AddAsync": AddAsyncMacro.self
]
func testUnwrapMacroExpansion() throws {
assertMacroExpansion(
"""
let value = #unwrap(optionalString)
""",
expandedSource: """
let value = {
guard let value = optionalString else {
fatalError("Failed to unwrap 'optionalString' - value was nil")
}
return value
}()
""",
macros: testMacros
)
}
func testAutoInitMacroWithStruct() throws {
assertMacroExpansion(
"""
@AutoInit
struct Point {
let x: Int
var y: Int
}
""",
expandedSource: """
struct Point {
let x: Int
var y: Int
public init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
""",
macros: testMacros
)
}
func testAutoInitWithDefaultValues() throws {
assertMacroExpansion(
"""
@AutoInit
struct Config {
var timeout: Int = 30
var retryCount: Int
}
""",
expandedSource: """
struct Config {
var timeout: Int = 30
var retryCount: Int
public init(timeout: Int = 30, retryCount: Int) {
self.timeout = timeout
self.retryCount = retryCount
}
}
""",
macros: testMacros
)
}
func testAutoInitFailsOnEnum() throws {
assertMacroExpansion(
"""
@AutoInit
enum Status {
case active
}
""",
expandedSource: """
enum Status {
case active
}
""",
diagnostics: [
DiagnosticSpec(
message: "@AutoInit can only be applied to structs and classes",
line: 1,
column: 1
)
],
macros: testMacros
)
}
}Os testes verificam a expansão correta do código e as mensagens de erro adequadas para usos inválidos.
Pronto para mandar bem nas entrevistas de iOS?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Macro avançada: property wrapper observável
Esta macro combina vários papéis para criar um sistema de observação de propriedades com notificações automáticas.
/// Adds automatic property change observation
@attached(accessor)
@attached(peer, names: prefixed(_))
public macro Observable() = #externalMacro(
module: "MyMacrosPlugin",
type: "ObservableMacro"
)import SwiftSyntax
import SwiftSyntaxMacros
// Implements both roles: accessor and peer
public enum ObservableMacro {}
extension ObservableMacro: AccessorMacro {
public static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntax,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax] {
guard let varDecl = declaration.as(VariableDeclSyntax.self),
let binding = varDecl.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else {
return []
}
let name = identifier.identifier.text
let storageName = "_\(name)"
// Generate get and set accessors
let getter: AccessorDeclSyntax = """
get {
access(keyPath: \\.\(raw: name))
return \(raw: storageName)
}
"""
let setter: AccessorDeclSyntax = """
set {
withMutation(keyPath: \\.\(raw: name)) {
\(raw: storageName) = newValue
}
}
"""
return [getter, setter]
}
}
extension ObservableMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let varDecl = declaration.as(VariableDeclSyntax.self),
let binding = varDecl.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
let type = binding.typeAnnotation?.type else {
return []
}
let name = identifier.identifier.text
let storageName = "_\(name)"
let typeName = type.description
// Generate private storage property
let initializer = binding.initializer.map { " \($0)" } ?? ""
let storageDecl: DeclSyntax = """
private var \(raw: storageName): \(raw: typeName)\(raw: initializer)
"""
return [storageDecl]
}
}Uso do padrão Observable
@Observable
class UserViewModel {
@Observable var name: String = ""
@Observable var age: Int = 0
@Observable var isActive: Bool = true
// Generated code for each property:
// private var _name: String = ""
// var name: String {
// get {
// access(keyPath: \.name)
// return _name
// }
// set {
// withMutation(keyPath: \.name) {
// _name = newValue
// }
// }
// }
}A Apple aplica esse mesmo padrão no novo framework Observation a partir do Swift 5.9.
Depurar e inspecionar macros
O Xcode oferece várias ferramentas para depurar macros e entender o código gerado.
Expansão no Xcode
// Right-click on macro call → "Expand Macro"
// Displays generated code inline
@AutoInit
struct Product {
let id: UUID
var name: String
var price: Decimal
}
// To see the expansion:
// 1. Right-click on @AutoInit
// 2. Select "Expand Macro"
// 3. Generated code displays inline for inspection and debuggingLogging durante o desenvolvimento
public struct DebugMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
// Print the node's AST to understand the structure
print("=== DEBUG MACRO ===")
print("Node: \(node)")
print("Arguments: \(node.argumentList)")
// Complete dump of the syntax tree
dump(node)
// Continue with normal expansion
return "42"
}
}Explorando a AST com o swift-ast-explorer
A ferramenta on-line swift-ast-explorer.com permite visualizar a árvore de sintaxe de qualquer código Swift. É indispensável para entender como percorrer os nós da AST ao implementar uma macro.
Boas práticas para Swift Macros
Criar macros sustentáveis exige seguir certas convenções e evitar armadilhas comuns.
Validação e mensagens de erro
public struct ValidatedMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// ✅ Validate usage context
guard declaration.is(StructDeclSyntax.self) else {
// ✅ Clear error messages with possible localization
context.diagnose(
Diagnostic(
node: node,
message: MacroDiagnosticMessage(
id: "invalid-target",
message: "This macro can only be applied to structs",
severity: .error
)
)
)
return []
}
// ✅ Check required arguments
guard let arguments = node.arguments else {
context.diagnose(
Diagnostic(
node: node,
message: MacroDiagnosticMessage(
id: "missing-args",
message: "Required arguments missing",
severity: .error
)
)
)
return []
}
// Implementation...
return []
}
}
// Structure for diagnostic messages
struct MacroDiagnosticMessage: DiagnosticMessage {
let id: String
let message: String
let severity: DiagnosticSeverity
var diagnosticID: MessageID {
MessageID(domain: "MyMacros", id: id)
}
}Gerar código legível
// ❌ Hard-to-read generated code
let badCode: DeclSyntax = "public init(a:Int,b:String,c:Bool){self.a=a;self.b=b;self.c=c}"
// ✅ Properly formatted generated code
let goodCode: DeclSyntax = """
public init(
a: Int,
b: String,
c: Bool
) {
self.a = a
self.b = b
self.c = c
}
"""O código gerado precisa ser tão legível quanto o escrito à mão, já que quem desenvolve vai inspecioná-lo via "Expand Macro".
Conclusão
As Swift Macros são uma ferramenta poderosa para eliminar boilerplate sem abrir mão da segurança de tipos estática. Essa tecnologia permite:
Pontos-chave:
- ✅ Duas categorias: freestanding (
#) e attached (@) - ✅ Cinco papéis attached: peer, accessor, member, memberAttribute, conformance
- ✅ Implementação via swift-syntax e manipulação da AST
- ✅ Testes obrigatórios com
SwiftSyntaxMacrosTestSupport - ✅ Pacote separado obrigatório para as implementações
- ✅ Depuração via "Expand Macro" no Xcode
- ✅ Mensagens de erro explícitas, essenciais para a experiência de quem desenvolve
As Swift Macros são particularmente úteis para gerar conformances (Equatable, Codable), criar property wrappers avançados e modernizar APIs baseadas em callbacks rumo ao async/await.
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Tags
Compartilhar
Artigos relacionados

Combine vs async/await em Swift: Padrões de Migração Progressiva
Guia completo para migrar de Combine para async/await em Swift: estratégias progressivas, padrões de ponte e coexistência de paradigmas em bases de código iOS.

Perguntas de entrevista sobre acessibilidade iOS em 2026: VoiceOver e Dynamic Type
Prepare-se para entrevistas iOS com perguntas-chave de acessibilidade: VoiceOver, Dynamic Type, traits semânticos e auditorias.

Entrevista StoreKit 2: Gerenciamento de Assinaturas e Validação de Recibos
Domine perguntas de entrevista iOS sobre StoreKit 2, gerenciamento de assinaturas, validação de recibos e implementação de compras no aplicativo com exemplos práticos em Swift.