Swift Macros : exemples pratiques de métaprogrammation

Guide complet sur Swift Macros : création de macros freestanding et attached, manipulation de l'AST avec swift-syntax, et exemples pratiques pour réduire le boilerplate.

Swift Macros métaprogrammation avec exemples de code pratiques

Les Swift Macros, introduites avec Swift 5.9 et Xcode 15, représentent une révolution dans la façon d'écrire du code Swift. Cette fonctionnalité permet de générer du code à la compilation, éliminant le boilerplate tout en conservant la sécurité du typage statique. Contrairement aux macros C préprocesseur, les macros Swift sont type-safe, intégrées au compilateur et supportées par les outils de développement.

Ce que couvre ce guide

Ce guide explore la création de macros Swift de A à Z : des concepts fondamentaux aux implémentations avancées, avec des exemples de code fonctionnels prêts à être adaptés dans tout projet iOS.

Comprendre les types de macros Swift

Swift propose deux catégories principales de macros, chacune avec des cas d'usage distincts. Les macros freestanding s'utilisent de manière autonome comme des expressions ou déclarations, tandis que les macros attached se rattachent à des déclarations existantes pour les modifier ou les enrichir.

Macros Freestanding : expression et déclaration

Les macros freestanding commencent par le symbole # et peuvent soit retourner une valeur (expression), soit créer de nouvelles déclarations. Voici un exemple concret de macro expression :

MacroUsage.swiftswift
// Macro freestanding expression - génère une valeur
let buildInfo = #buildDate
// Expansion → "2026-03-11 10:30:45"

// Macro freestanding avec arguments
let message = #stringify(1 + 2)
// Expansion → "1 + 2 = 3"

// Macro freestanding declaration - crée des déclarations
#makeCase("success", "failure", "pending")
// Expansion →
// case success
// case failure
// case pending

La différence fondamentale entre expression et déclaration réside dans le résultat : une expression produit une valeur, une déclaration produit du code structurel (types, fonctions, variables).

Macros Attached : les cinq rôles

Les macros attached commencent par @ et se placent devant une déclaration. Swift définit cinq rôles distincts pour ces macros :

AttachedMacroRoles.swiftswift
// @attached(peer) - ajoute des déclarations au même niveau
@AddAsync
func fetchUser(id: Int) -> User { ... }
// Expansion → ajoute func fetchUserAsync(id: Int) async -> User

// @attached(accessor) - ajoute getters/setters
@UserDefault("theme")
var currentTheme: String
// Expansion → ajoute get { UserDefaults.standard.string(...) }

// @attached(member) - ajoute des membres à un type
@AutoEquatable
struct Point {
    var x: Int
    var y: Int
}
// Expansion → ajoute static func == (lhs: Point, rhs: Point) -> Bool

// @attached(memberAttribute) - applique des attributs aux membres
@CodableKeys
struct Config {
    var apiUrl: String
    var timeout: Int
}
// Expansion → ajoute @CodingKey("api_url") devant apiUrl

// @attached(conformance) / @attached(extension) - ajoute des conformances
@Hashable
struct User {
    var id: Int
    var name: String
}
// Expansion → ajoute extension User: Hashable { ... }

Ces rôles peuvent être combinés pour créer des macros puissantes qui transforment le code de plusieurs façons simultanément.

Configuration du projet pour créer des macros

La création de macros Swift nécessite un Swift Package avec une structure spécifique. Le package dépend de swift-syntax, la bibliothèque officielle pour manipuler le code Swift sous forme d'arbre syntaxique abstrait (AST).

Structure du Package.swift

Package.swiftswift
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyMacros",
    platforms: [.macOS(.v10_15), .iOS(.v13)],
    products: [
        // Bibliothèque exposant les macros au projet principal
        .library(
            name: "MyMacros",
            targets: ["MyMacros"]
        ),
        // Exécutable pour tester les macros
        .executable(
            name: "MyMacrosClient",
            targets: ["MyMacrosClient"]
        )
    ],
    dependencies: [
        // Dépendance obligatoire pour les macros
        .package(
            url: "https://github.com/apple/swift-syntax.git",
            from: "509.0.0"
        )
    ],
    targets: [
        // Plugin de compilation contenant l'implémentation
        .macro(
            name: "MyMacrosPlugin",
            dependencies: [
                .product(name: "SwiftSyntax", package: "swift-syntax"),
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
            ]
        ),
        // Target exposant les déclarations de macros
        .target(
            name: "MyMacros",
            dependencies: ["MyMacrosPlugin"]
        ),
        // Client de test
        .executableTarget(
            name: "MyMacrosClient",
            dependencies: ["MyMacros"]
        ),
        // Tests unitaires
        .testTarget(
            name: "MyMacrosTests",
            dependencies: [
                "MyMacrosPlugin",
                .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
            ]
        )
    ]
)

Cette configuration sépare clairement la déclaration des macros (ce que le code client voit) de leur implémentation (exécutée à la compilation).

Organisation recommandée

Trois fichiers minimum sont nécessaires : MyMacros.swift pour les déclarations, MyMacrosPlugin.swift pour les implémentations, et MyMacrosTests.swift pour les tests. Cette séparation facilite la maintenance.

Création d'une macro Expression

Les macros expression génèrent une valeur utilisable dans le code. Voici comment créer une macro #unwrap qui unwrappe un optionnel avec un message d'erreur personnalisé incluant le nom de la variable.

Déclaration de la macro

MyMacros.swiftswift
import Foundation

/// Macro qui unwrappe un optionnel avec un message d'erreur explicite
/// 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"
)

La signature déclare que la macro prend un optionnel et retourne la valeur non-optionnelle. #externalMacro pointe vers l'implémentation dans le plugin.

Implémentation avec 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 {
        // Récupère le premier argument passé à la macro
        guard let argument = node.argumentList.first?.expression else {
            throw MacroError.missingArgument
        }

        // Extrait le nom de la variable pour le message d'erreur
        let variableName = argument.description.trimmingCharacters(
            in: .whitespacesAndNewlines
        )

        // Génère le code d'expansion
        // Utilise une closure immédiatement exécutée pour encapsuler le guard
        return """
            {
                guard let value = \(argument) else {
                    fatalError("Failed to unwrap '\\(\(literal: variableName))' - value was nil")
                }
                return value
            }()
            """
    }
}

// Erreurs personnalisées pour les macros
enum MacroError: Error, CustomStringConvertible {
    case missingArgument
    case invalidSyntax(String)

    var description: String {
        switch self {
        case .missingArgument:
            return "La macro nécessite un argument"
        case .invalidSyntax(let message):
            return "Syntaxe invalide: \(message)"
        }
    }
}

La méthode expansion reçoit le nœud AST représentant l'appel de macro et le contexte de compilation. Elle retourne une ExprSyntax contenant le code généré.

Enregistrement du plugin

MyMacrosPlugin.swiftswift
import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct MyMacrosPlugin: CompilerPlugin {
    // Liste toutes les macros fournies par ce plugin
    let providingMacros: [Macro.Type] = [
        UnwrapMacro.self,
        // Autres macros à ajouter ici
    ]
}

Ce point d'entrée informe le compilateur des macros disponibles dans ce plugin.

Prêt à réussir tes entretiens iOS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Création d'une macro Attached Member

Les macros member ajoutent des membres (propriétés, méthodes, types imbriqués) à un type existant. Voici une macro @AutoInit qui génère automatiquement un initialiseur memberwise.

Déclaration et implémentation complète

MyMacros.swiftswift
/// Génère automatiquement un initialiseur avec tous les 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] {
        // Vérifie que la macro est appliquée à une struct ou class
        guard declaration.is(StructDeclSyntax.self) ||
              declaration.is(ClassDeclSyntax.self) else {
            throw MacroError.invalidSyntax(
                "@AutoInit ne peut être appliqué qu'aux structs et classes"
            )
        }

        // Collecte les stored properties
        let properties = declaration.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .filter { isStoredProperty($0) }

        // Génère les paramètres de l'initialiseur
        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)

            // Vérifie si la propriété a une valeur par défaut
            if binding.initializer != nil {
                return "\(name): \(typeName) = \(binding.initializer!.value)"
            }
            return "\(name): \(typeName)"
        }

        // Génère les assignations dans le corps de l'init
        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)"
        }

        // Construit l'initialiseur complet
        let initDecl: DeclSyntax = """
            public init(\(raw: parameters.joined(separator: ", "))) {
                \(raw: assignments.joined(separator: "\n        "))
            }
            """

        return [initDecl]
    }

    // Vérifie si une variable est une stored property (pas computed)
    private static func isStoredProperty(_ variable: VariableDeclSyntax) -> Bool {
        guard let binding = variable.bindings.first else { return false }

        // Une computed property a un accessor block avec get/set
        if let accessor = binding.accessorBlock {
            // Si c'est un bloc avec des accesseurs explicites, c'est computed
            if accessor.accessors.is(AccessorDeclListSyntax.self) {
                return false
            }
        }

        // let ou var sans accesseur = stored property
        return true
    }
}

Utilisation de la macro AutoInit

UserModel.swiftswift
@AutoInit
struct User {
    let id: UUID
    var name: String
    var email: String
    var isActive: Bool = true
}

// Code généré automatiquement :
// public init(id: UUID, name: String, email: String, isActive: Bool = true) {
//     self.id = id
//     self.name = name
//     self.email = email
//     self.isActive = isActive
// }

// Utilisation
let user = User(id: UUID(), name: "Alice", email: "alice@example.com")
// isActive utilise la valeur par défaut

Cette macro élimine le boilerplate des initialiseurs, particulièrement utile pour les modèles de données avec de nombreuses propriétés.

Macro Attached Peer pour génération async

Les macros peer ajoutent des déclarations au même niveau que la déclaration annotée. Voici une macro @AddAsync qui génère une version async d'une fonction completion-based.

MyMacros.swiftswift
/// Génère automatiquement une version async d'une fonction avec 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] {
        // Vérifie que c'est une fonction
        guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
            throw MacroError.invalidSyntax(
                "@AddAsync requiert une fonction"
            )
        }

        let functionName = funcDecl.name.text
        let asyncFunctionName = "\(functionName)Async"

        // Analyse les paramètres pour trouver le completion handler
        let parameters = funcDecl.signature.parameterClause.parameters

        // Filtre les paramètres (exclut le completion handler)
        var regularParams: [String] = []
        var completionType: String? = nil

        for param in parameters {
            let paramType = param.type.description

            // Détecte un completion handler (closure avec Result ou valeur simple)
            if paramType.contains("->") && paramType.contains("Void") {
                // Extrait le type de retour du 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(
                "Aucun completion handler trouvé"
            )
        }

        // Génère les arguments pour l'appel interne
        let callArgs = parameters.dropLast().map { param in
            let name = param.firstName.text
            return "\(name): \(name)"
        }.joined(separator: ", ")

        // Génère la fonction async
        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]
    }

    // Extrait le type de retour depuis un type Result
    private static func extractCompletionReturnType(from type: String) -> String {
        // Pattern simplifié - en production, utiliser l'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"
    }
}

Démonstration de la macro AddAsync

NetworkService.swiftswift
class NetworkService {
    @AddAsync
    func fetchUser(
        id: Int,
        completion: @escaping (Result<User, Error>) -> Void
    ) {
        // Implémentation avec 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()
    }

    // Génère automatiquement :
    // 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)
    //             }
    //         })
    //     }
    // }
}

// Utilisation moderne avec async/await
let user = try await networkService.fetchUserAsync(id: 42)
Limitations des macros peer

Le nom de la fonction générée doit être déclaré dans names: de l'attribut @attached. Ici, suffixed(Async) indique que la fonction générée aura le suffixe "Async" ajouté au nom original.

Tests unitaires des macros

Tester les macros est essentiel car elles génèrent du code qui sera compilé. Swift fournit SwiftSyntaxMacrosTestSupport pour faciliter ces tests.

MyMacrosTests.swiftswift
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import MyMacrosPlugin

final class MyMacrosTests: XCTestCase {

    // Dictionnaire des macros à tester
    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 ne peut être appliqué qu'aux structs et classes",
                    line: 1,
                    column: 1
                )
            ],
            macros: testMacros
        )
    }
}

Les tests vérifient l'expansion correcte du code et les messages d'erreur appropriés pour les usages invalides.

Prêt à réussir tes entretiens iOS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Macro avancée : Observable Property Wrapper

Cette macro combine plusieurs rôles pour créer un système d'observation de propriétés avec notifications automatiques.

MyMacros.swiftswift
/// Ajoute l'observation automatique des changements de propriété
@attached(accessor)
@attached(peer, names: prefixed(_))
public macro Observable() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "ObservableMacro"
)
ObservableMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

// Implémente les deux rôles : accessor et 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)"

        // Génère les accesseurs get et set
        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

        // Génère la propriété de stockage privée
        let initializer = binding.initializer.map { " \($0)" } ?? ""
        let storageDecl: DeclSyntax = """
            private var \(raw: storageName): \(raw: typeName)\(raw: initializer)
            """

        return [storageDecl]
    }
}

Utilisation du pattern Observable

ViewModel.swiftswift
@Observable
class UserViewModel {
    @Observable var name: String = ""
    @Observable var age: Int = 0
    @Observable var isActive: Bool = true

    // Code généré pour chaque propriété :
    // private var _name: String = ""
    // var name: String {
    //     get {
    //         access(keyPath: \.name)
    //         return _name
    //     }
    //     set {
    //         withMutation(keyPath: \.name) {
    //             _name = newValue
    //         }
    //     }
    // }
}

Ce pattern est utilisé par Apple dans le nouveau framework Observation de Swift 5.9+.

Debugging et inspection des macros

Xcode offre plusieurs outils pour debugger les macros et comprendre le code généré.

Expansion dans Xcode

DebuggingMacros.swiftswift
// Clic droit sur l'appel de macro → "Expand Macro"
// Affiche le code généré inline

@AutoInit
struct Product {
    let id: UUID
    var name: String
    var price: Decimal
}

// Pour voir l'expansion :
// 1. Clic droit sur @AutoInit
// 2. Sélectionner "Expand Macro"
// 3. Le code généré s'affiche inline, permettant inspection et debug

Logging pendant le développement

DebugMacro.swiftswift
public struct DebugMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        // Affiche l'AST du nœud pour comprendre la structure
        print("=== DEBUG MACRO ===")
        print("Node: \(node)")
        print("Arguments: \(node.argumentList)")

        // Dump complet de l'arbre syntaxique
        dump(node)

        // Continue avec l'expansion normale
        return "42"
    }
}

Explorer l'AST avec swift-ast-explorer

L'outil en ligne swift-ast-explorer.com permet de visualiser l'arbre syntaxique de n'importe quel code Swift. C'est indispensable pour comprendre comment naviguer dans les nœuds AST lors de l'implémentation de macros.

Bonnes pratiques pour les macros Swift

La création de macros maintenables requiert de suivre certaines conventions et d'éviter les pièges courants.

Validation et messages d'erreur

ValidationBestPractices.swiftswift
public struct ValidatedMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // ✅ Valider le contexte d'utilisation
        guard declaration.is(StructDeclSyntax.self) else {
            // ✅ Messages d'erreur clairs avec localisation possible
            context.diagnose(
                Diagnostic(
                    node: node,
                    message: MacroDiagnosticMessage(
                        id: "invalid-target",
                        message: "Cette macro ne peut être appliquée qu'aux structs",
                        severity: .error
                    )
                )
            )
            return []
        }

        // ✅ Vérifier les arguments requis
        guard let arguments = node.arguments else {
            context.diagnose(
                Diagnostic(
                    node: node,
                    message: MacroDiagnosticMessage(
                        id: "missing-args",
                        message: "Arguments requis manquants",
                        severity: .error
                    )
                )
            )
            return []
        }

        // Implémentation...
        return []
    }
}

// Structure pour les messages de diagnostic
struct MacroDiagnosticMessage: DiagnosticMessage {
    let id: String
    let message: String
    let severity: DiagnosticSeverity

    var diagnosticID: MessageID {
        MessageID(domain: "MyMacros", id: id)
    }
}

Générer du code lisible

ReadableCodeGeneration.swiftswift
// ❌ Code généré difficile à lire
let badCode: DeclSyntax = "public init(a:Int,b:String,c:Bool){self.a=a;self.b=b;self.c=c}"

// ✅ Code généré avec formatage correct
let goodCode: DeclSyntax = """
    public init(
        a: Int,
        b: String,
        c: Bool
    ) {
        self.a = a
        self.b = b
        self.c = c
    }
    """

Le code généré doit être aussi lisible que du code écrit manuellement, car les développeurs l'inspecteront via "Expand Macro".

Conclusion

Les Swift Macros représentent un outil puissant pour éliminer le boilerplate tout en conservant la sécurité du typage statique. Cette technologie permet de :

Points clés à retenir :

  • ✅ Deux catégories : freestanding (#) et attached (@)
  • ✅ Cinq rôles attached : peer, accessor, member, memberAttribute, conformance
  • ✅ Implémentation via swift-syntax et manipulation de l'AST
  • ✅ Tests obligatoires avec SwiftSyntaxMacrosTestSupport
  • ✅ Package séparé requis pour les implémentations
  • ✅ Debugging via "Expand Macro" dans Xcode
  • ✅ Messages d'erreur explicites essentiels pour l'UX développeur

Les macros Swift sont particulièrement utiles pour générer des conformances (Equatable, Codable), créer des property wrappers avancés, et moderniser des API callback-based vers async/await.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#swift
#ios
#macros
#metaprogrammation
#swift-syntax

Partager

Articles similaires