Swift Macros: praktische voorbeelden van metaprogrammering

Volledige gids over Swift Macros: het maken van freestanding- en attached-macro's inclusief Swift 6 body macro's, AST-manipulatie met swift-syntax en praktische voorbeelden om boilerplate-code te elimineren.

Metaprogrammering met Swift Macros en praktische codevoorbeelden

Swift Macros transformeren de manier waarop ontwikkelaars code schrijven en onderhouden. Geïntroduceerd met Swift 5.9 en aanzienlijk uitgebreid in Swift 6, maken macro's codegeneratie tijdens compilatie mogelijk met behoud van statische type-veiligheid. In tegenstelling tot de macro's van de C-preprocessor zijn Swift Macros type-veilig, in de compiler geïntegreerd en volledig ondersteund door de ontwikkeltools, inclusief de "Expand Macro" functie van Xcode.

Wat deze gids behandelt

Deze gids behandelt het maken van Swift Macros van fundamentele concepten tot geavanceerde implementaties, inclusief de nieuwe body macro's geïntroduceerd in Swift 6 (SE-0415). Alle voorbeelden gebruiken Swift 6 en swift-syntax 600.0.0+.

De typen Swift Macros begrijpen

Swift biedt twee hoofdcategorieën macro's, elk met eigen toepassingen. Freestanding-macro's werken zelfstandig als expressie of declaratie, terwijl attached-macro's zich aan bestaande declaraties binden om deze aan te passen of uit te breiden.

Freestanding-macro's: expressie en declaratie

Freestanding-macro's beginnen met het symbool # en kunnen ofwel een waarde teruggeven (expressie) ofwel nieuwe declaraties creëren. Hier een concreet voorbeeld van een expressie-macro:

MacroUsage.swiftswift
// Freestanding expression macro - generates a value
let buildInfo = #buildDate
// Expansion → "2026-08-23 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 pending

Het wezenlijke verschil tussen expressie en declaratie zit in het resultaat: een expressie levert een waarde, een declaratie genereert structurele code (typen, functies, variabelen).

Attached-macro's: de zes rollen

Attached-macro's beginnen met @ en staan voor een declaratie. Swift 6 definieert zes verschillende rollen voor deze macro's, inclusief de nieuwe body-rol voor functie-implementaties:

AttachedMacroRoles.swiftswift
// @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(extension) - adds protocol conformances
@Hashable
struct User {
    var id: Int
    var name: String
}
// Expansion → adds extension User: Hashable { ... }

// @attached(body) - NEW in Swift 6: synthesizes function bodies
@Remote
func fetchData(endpoint: String) -> Data
// Expansion → generates the entire function implementation

Deze rollen kunnen worden gecombineerd om krachtige macro's te bouwen die code op meerdere dimensies tegelijk transformeren.

Swift 6 Body Macros: SE-0415

Swift 6 introduceerde function body macro's via SE-0415, waarmee macro's functie-implementaties kunnen synthetiseren of vervangen. Dit lost use cases op die geen eerdere macro-rol kon afhandelen.

Het BodyMacro-protocol

Het BodyMacro-protocol maakt volledige vervanging van een functiebody mogelijk:

BodyMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

public protocol BodyMacro: AttachedMacro {
    static func expansion(
        of node: AttributeSyntax,
        providingBodyFor declaration: some DeclSyntaxProtocol &
            WithOptionalCodeBlockSyntax,
        in context: some MacroExpansionContext
    ) throws -> [CodeBlockItemSyntax]
}

De macro ontvangt de originele declaratie en retourneert code block items die de functiebody worden. Een bestaande body wordt vervangen.

Implementatie van een Remote Procedure Call macro

Body macro's zijn bijzonder nuttig voor het genereren van netwerklaagcode uit functiesignaturen:

RemoteMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

public struct RemoteMacro: BodyMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingBodyFor declaration: some DeclSyntaxProtocol &
            WithOptionalCodeBlockSyntax,
        in context: some MacroExpansionContext
    ) throws -> [CodeBlockItemSyntax] {
        guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
            throw MacroError.invalidSyntax("@Remote requires a function")
        }
        
        let functionName = funcDecl.name.text
        let parameters = funcDecl.signature.parameterClause.parameters
        
        // Generate parameter encoding
        let paramDict = parameters.map { param in
            let name = param.firstName.text
            return "\"\(name)\": \(name)"
        }.joined(separator: ", ")
        
        // Generate the RPC body
        return [
            """
            let params: [String: Any] = [\(raw: paramDict)]
            return try await RPCClient.shared.call(
                method: "\(raw: functionName)",
                params: params
            )
            """
        ]
    }
}

Declaratie en gebruik

MyMacros.swiftswift
/// Generates a remote procedure call implementation
@attached(body)
public macro Remote() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "RemoteMacro"
)
APIService.swiftswift
class APIService {
    @Remote
    func createUser(name: String, email: String) async throws -> User
    
    @Remote
    func deleteUser(id: UUID) async throws -> Bool
    
    // Each function body is generated at compile time:
    // func createUser(name: String, email: String) async throws -> User {
    //     let params: [String: Any] = ["name": name, "email": email]
    //     return try await RPCClient.shared.call(
    //         method: "createUser",
    //         params: params
    //     )
    // }
}
Beperking van body macro's

Maximaal één body macro kan worden toegepast op een gegeven functie. Voor componeerbare preamble-injectie (logging, tracing) wordt het PreambleMacro-protocol overwogen in toekomstige Swift-versies.

Projectopzet voor het maken van macro's

Het maken van Swift Macros vereist een Swift Package met een specifieke structuur. Het package is afhankelijk van swift-syntax, de officiële bibliotheek om Swift-code te bewerken als abstracte syntaxboom (AST).

Structuur van Package.swift

Package.swiftswift
// swift-tools-version: 6.0
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 - use 600.0.0+ for Swift 6
        .package(
            url: "https://github.com/swiftlang/swift-syntax.git",
            from: "600.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")
            ]
        )
    ]
)

Deze configuratie scheidt de macro-declaraties (wat de cliëntcode ziet) duidelijk van de implementatie (uitgevoerd tijdens compilatie).

Aanbevolen indeling

Minimaal drie bestanden zijn nodig: MyMacros.swift voor de declaraties, MyMacrosPlugin.swift voor de implementaties en MyMacrosTests.swift voor de tests. Deze scheiding vergemakkelijkt het onderhoud.

Een expressie-macro maken

Expressie-macro's genereren een waarde die direct in de code bruikbaar is. Hier wordt een macro #unwrap gemaakt die een optional uitpakt met een aangepaste foutmelding waarin de variabelenaam staat. Meer informatie over foutafhandelingspatronen in Swift is te vinden in de gids over Swift Structured Concurrency.

Declaratie van de macro

MyMacros.swiftswift
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"
)

De signatuur stelt dat de macro een optional accepteert en de niet-optionele waarde retourneert. #externalMacro verwijst naar de implementatie binnen de plugin.

Implementatie met swift-syntax

UnwrapMacro.swiftswift
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)"
        }
    }
}

De methode expansion ontvangt de AST-node die de macro-aanroep voorstelt en de compilatiecontext. Ze retourneert een ExprSyntax met de gegenereerde code.

Plugin-registratie

MyMacrosPlugin.swiftswift
import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct MyMacrosPlugin: CompilerPlugin {
    // List all macros provided by this plugin
    let providingMacros: [Macro.Type] = [
        UnwrapMacro.self,
        RemoteMacro.self,
        AutoInitMacro.self,
        // Add other macros here
    ]
}

Dit instappunt informeert de compiler over de macro's die deze plugin aanbiedt.

Klaar om je iOS gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Een attached member-macro maken

Member-macro's voegen leden toe (eigenschappen, methoden, geneste typen) aan een bestaand type. Hier volgt een macro @AutoInit die automatisch een memberwise-initializer genereert.

Volledige declaratie en implementatie

MyMacros.swiftswift
/// Automatically generates an initializer with all stored properties
@attached(member, names: named(init))
public macro AutoInit() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "AutoInitMacro"
)
AutoInitMacro.swiftswift
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
    }
}

Gebruik van de AutoInit-macro

UserModel.swiftswift
@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 value

Deze macro elimineert de boilerplate van de initializer en is bijzonder nuttig bij datamodellen met veel eigenschappen.

Attached peer-macro voor async-generatie

Peer-macro's voegen declaraties toe op hetzelfde niveau als de geannoteerde declaratie. Hier volgt een macro @AddAsync die een async-versie genereert van een functie met een completion handler. Dit patroon is bijzonder relevant bij het migreren van legacy netwerkcode, zoals behandeld in de Combine vs async/await migratiegids.

MyMacros.swiftswift
/// Automatically generates an async version of a function with completion handler
@attached(peer, names: suffixed(Async))
public macro AddAsync() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "AddAsyncMacro"
)
AddAsyncMacro.swiftswift
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"
    }
}

Demonstratie van de AddAsync-macro

NetworkService.swiftswift
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)
Beperkingen van peer-macro's

De naam van de gegenereerde functie moet worden gedeclareerd in names: van het @attached-attribuut. Hier geeft suffixed(Async) aan dat de gegenereerde functie het achtervoegsel "Async" krijgt na de oorspronkelijke naam.

Unittests voor macro's

Het testen van macro's is essentieel, omdat ze code genereren die wordt gecompileerd. Swift biedt SwiftSyntaxMacrosTestSupport om deze tests te vereenvoudigen. Vanaf swift-syntax 603.0.0+ wordt naast XCTest ook Swift Testing ondersteund.

MyMacrosTests.swiftswift
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,
        "Remote": RemoteMacro.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
        )
    }

    func testRemoteMacroExpansion() throws {
        assertMacroExpansion(
            """
            @Remote
            func fetchUser(id: Int) async throws -> User
            """,
            expandedSource: """
            func fetchUser(id: Int) async throws -> User {
                let params: [String: Any] = ["id": id]
                return try await RPCClient.shared.call(
                    method: "fetchUser",
                    params: params
                )
            }
            """,
            macros: testMacros
        )
    }
}

De tests verifiëren de juiste expansie van de code en passende foutmeldingen bij ongeldig gebruik.

Macro's debuggen en inspecteren

Xcode biedt verschillende tools om macro's te debuggen en de gegenereerde code te begrijpen.

Expansie in Xcode

DebuggingMacros.swiftswift
// 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 debugging

Logging tijdens de ontwikkeling

DebugMacro.swiftswift
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"
    }
}

De AST verkennen met swift-ast-explorer

De online tool swift-ast-explorer.com toont de syntaxboom van willekeurige Swift-code. Het begrijpen van de AST-nodestructuur is essentieel bij het implementeren van macro's, zoals gedemonstreerd in de Swift Testing framework gids die de in Swift ingebouwde test-macro's behandelt.

Best practices voor Swift Macros

Onderhoudbare macro's vereisen het naleven van bepaalde conventies en het vermijden van veelvoorkomende valkuilen.

Validatie en foutmeldingen

ValidationBestPractices.swiftswift
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)
    }
}

Leesbare code genereren

ReadableCodeGeneration.swiftswift
// Avoid: 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}"

// Preferred: properly formatted generated code
let goodCode: DeclSyntax = """
    public init(
        a: Int,
        b: String,
        c: Bool
    ) {
        self.a = a
        self.b = b
        self.c = c
    }
    """

De gegenereerde code moet net zo leesbaar zijn als handgeschreven code, want ontwikkelaars zullen die inspecteren via "Expand Macro".

Sources

Wat Swift Macros in de praktijk mogelijk maken

Swift Macros vormen een krachtig hulpmiddel om boilerplate te elimineren met behoud van statische type-veiligheid. Deze technologie maakt het volgende mogelijk:

  • Twee categorieën macro's: freestanding (#) en attached (@)
  • Zes attached-rollen in Swift 6: peer, accessor, member, memberAttribute, extension en de nieuwe body-rol
  • Implementatie via swift-syntax 600.0.0+ en AST-manipulatie
  • Verplichte tests met SwiftSyntaxMacrosTestSupport, nu met ondersteuning voor Swift Testing
  • Aparte package vereist voor de implementaties
  • Debuggen via "Expand Macro" in Xcode
  • Duidelijke foutmeldingen zijn essentieel voor de developer experience

Swift Macros zijn vooral nuttig voor het genereren van conformances (Equatable, Codable), het bouwen van geavanceerde property wrappers, het synthetiseren van RPC-implementaties uit signaturen en het moderniseren van API's gebaseerd op callbacks naar async/await.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Dagelijkse challenge

Zie jij de bug in iOS?

Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Anthony Fillion-Maillet

Geschreven door

Anthony Fillion-Maillet

Oprichter van SharpSkill

Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.

Bijgewerkt op 23 augustus 2026

Tags

#swift
#ios
#macros
#metaprogramming
#swift-syntax

Delen

Gerelateerde artikelen