Swift Macros: Saját makró fejlesztése Swift 6-ban (2026)

Lépésről lépésre útmutató saját Swift makró írásához Swift 6-ban: deklaráció, SwiftSyntax plugin felépítése, @attached példa, diagnosztikák és tesztelés assertMacroExpansion-nel.

Swift Macros útmutató Swift 6-ban (2026)

Frissítve: 2026. június 23.

A Swift makrók fordításidőben kódot generáló mechanizmusok, amelyek a forráskód absztrakt szintaxisfáját (AST) alakítják át a SwiftSyntax könyvtár segítségével, mindezt biztonságosan, típusellenőrzött módon, anélkül, hogy futásidejű reflektivitást vagy preprocesszor trükköket igényelnének. A Swift 5.9-ben mutatkoztak be, és Swift 6-ra már a standard könyvtár belső szövete: az #Predicate, az @Observable és a SwiftData @Model mind makrók. Ebben az útmutatóban végigvezetlek azon, hogyan írj saját makrót a nulláról: kezdve a deklaráción át a SwiftSyntax plugin felépítésén, a diagnosztikákon, az unit teszteken és a leggyakoribb buktatókon.

  • A Swift makrók két fő típusa a @freestanding (önálló kifejezés, pl. #stringify) és az @attached (deklarációhoz kapcsolt, pl. @Observable). Minden makró egy vagy több szerepkört (role) tölt be.
  • A makrók külön CompilerPlugin targetben élnek, ezért egy Swift Package-ben mindig három target tartozik hozzájuk: macros, a kliens könyvtár, és a tesztek.
  • A SwiftSyntax 600.x sorozat a Swift 6.0+ fordítóhoz tartozik; mindig azt a verziót kell rögzíteni a Package.swift-ben, ami a célzott Xcode verzióhoz illeszkedik.
  • A makrók fordításidőben futnak egy sandboxolt folyamatban, így nem érhetnek el fájlrendszert, hálózatot vagy a futásidejű környezetet.
  • A makró tesztelés szabványos eszköze a SwiftSyntaxMacrosTestSupport modul assertMacroExpansion függvénye, ami a generált forráskódot szövegszinten hasonlítja össze.
  • Az Xcode 26-ban a makrók aláíratlan plugin-ek, ezért első futtatáskor a felhasználónak engedélyt kell adnia a "Trust & Enable" gombbal. Ez gyakori CI hiba forrása.

Mi az a Swift macro és mikor érdemes használni?

A Swift macro egy fordításidőben végrehajtódó kódgeneráló komponens, amely az aktuális forrásdarab szintaxisfáját bemenetként kapja, és új vagy módosított szintaxiscsomópontokat ad vissza. A makró kimenete mindig Swift forráskód, amit a fordító ezután ugyanúgy típusellenőriz, mintha kézzel írtad volna. Ez a kulcskülönbség a régi C preprocesszor makrókhoz képest: a Swiftben nincs szöveges helyettesítés, csak strukturált AST transzformáció.

Őszintén szólva, Objective-C korszakot megjárt fejlesztőként sokáig idegenkedtem a makróktól. Túl könnyen lett belőlük olvashatatlan káosz. A Swift implementáció azonban más: a makrók nyilvánosan deklarálják, milyen kódot generálnak (a @attached(member, names: ...) szintaxis konkrétan kötelezi a fejlesztőt a generált név megnevezésére), és a fejlesztőeszközök képesek megmutatni a kibővített eredményt. Xcode-ban jobb klikk a makró hívásra, majd "Expand Macro". Mellesleg ez a legjobb tanulási módszer is.

Makrót akkor érdemes írni, ha (1) a boilerplate kódgenerálás típusalapú, és így nem oldható meg generikus függvénnyel; (2) ugyanazt a mintát több tízszer kell ismételni a projektben; (3) a generált kód olvashatóbb, ha látható, mint ha runtime reflection mögé van rejtve. Például egy @CodingKeys makró, ami a property nevekből generál CodingKey enumot, sokkal tisztább, mint a régi kézi sablonok. Ha érdekel az alapok mélyebb háttere, érdemes elolvasni a Swift 6.2 párhuzamossági útmutatót is, mert sok makró (pl. @Observable) szorosan együttműködik a concurrency rendszerrel.

Freestanding vs. attached makrók: melyiket válaszd?

A Swift két makró kategóriát ismer, és a választás a hívási helytől függ. A freestanding makró önálló kifejezés vagy deklaráció pozícióban hívódik a # jellel, például #stringify(x + y) egy kifejezést alakít át (value, "x + y") tuple-lé. Az attached makró egy deklarációhoz csatolódik a @ jellel, és kiegészíti azt, például a @Observable tagokat ad a class-hoz.

Tulajdonság@freestanding@attached
Szintaxis#myMacro(...)@MyMacro egy deklaráción
Tipikus szerepkörökexpression, declaration, codeItemmember, peer, accessor, memberAttribute, extension
Beépített példa#Predicate, #warning@Observable, @Model
Tipikus használatEgyszeri kódgenerálás kifejezésszintenType-szintű boilerplate (property, init, conformance)
Hozzáfér a célzott deklarációhozNemIgen (a declaration paraméteren át)

Az attached makróknak több szerepkörük lehet egyszerre, és a sorrend számít: először a memberAttribute fut, ami további attributumokat tehet meg a tagokra; utána a member, ami új tagokat ad; majd az accessor, ami getter/setter párokat generál. A @Observable például mind a hármat használja: a property-kre rákapcsolja az @ObservationTracked-et, hozzáad egy _$observationRegistrar tárolt mezőt, és minden trackelt property-re generál egy access/withMutation getter-settert.

Macro package felépítése Swift 6-ban

Minden makró egy Swift Package-ben él, három targettel: a plugin target (típusa .macro), ami fordításidőben futtatható; a kliens könyvtár, ami a publikus makró deklarációkat exportálja; és a teszt target. Az Xcode-ban a "File → New → Package → Swift Macro" sablon ezt automatikusan generálja, de érdemes manuálisan is tudni felépíteni, különösen, ha meglévő projektbe integrálsz.

A Package.swift a következőképp néz ki egy Swift 6.0+ kompatibilis makró csomag esetén:

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

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

A SwiftSyntax verziókövetés az egyik leggyakoribb buktató: a 600.0.0 a Swift 6.0-hoz, a 601.0.0 a Swift 6.1-hez tartozik, és így tovább. Ha rosszul választasz, a fordító cryptic "could not load module" hibát dob. (Pont ebbe szaladtam bele egy CI átállásnál tavaly.) Az aktuális kompatibilitási mátrix mindig a swift-syntax GitHub release oldalán található.

Első saját @attached makró: lépésről lépésre

Készítsünk egy @AutoEquatable makrót, ami egy struct minden tárolt property-jére generál egy Equatable conformance-t. A kliens oldal így néz ki:

// MyMacros/AutoEquatable.swift
@attached(extension, conformances: Equatable, names: named(==))
public macro AutoEquatable() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "AutoEquatableMacro"
)

A makró deklaráció önmagában nem csinál semmit, csak elnevezi a plugin típust, ami megvalósítja. Az #externalMacro a fordítónak szól: amikor ezt a makrót látja, töltse be a megadott plugin modult és hívja meg a megadott típust. A plugin oldali implementáció:

// MyMacrosPlugin/AutoEquatableMacro.swift
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftCompilerPlugin

public struct AutoEquatableMacro: ExtensionMacro {
    public static func expansion(
        of node: AttributeSyntax,
        attachedTo declaration: some DeclGroupSyntax,
        providingExtensionsOf type: some TypeSyntaxProtocol,
        conformingTo protocols: [TypeSyntax],
        in context: some MacroExpansionContext
    ) throws -> [ExtensionDeclSyntax] {

        let storedProperties = declaration.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .filter { $0.bindings.allSatisfy { $0.accessorBlock == nil } }
            .flatMap { $0.bindings.compactMap { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text } }

        let comparisons = storedProperties
            .map { "lhs.\($0) == rhs.\($0)" }
            .joined(separator: " && ")

        let body = storedProperties.isEmpty ? "true" : comparisons

        let ext = try ExtensionDeclSyntax("extension \(type.trimmed): Equatable") {
            try FunctionDeclSyntax("public static func == (lhs: \(type.trimmed), rhs: \(type.trimmed)) -> Bool") {
                "return \(raw: body)"
            }
        }
        return [ext]
    }
}

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

Két dolog érdemes figyelemre. Először, az ExtensionDeclSyntax string-interpolált inicializálója egy DSL: a \(type.trimmed) beilleszti a célzott típus nevét (pl. User), a \(raw: body) pedig nyers szövegként szúrja be a feltétellistát. Másodszor, a stored property szűrés a számított property-ket dobja ki ($0.accessorBlock == nil). Ezt elfelejteni klasszikus hiba, én is rontottam már el így első körben.

SwiftSyntax alapok: AST navigálás és kódgenerálás

A SwiftSyntax minden Swift nyelvi konstrukciót egy immutable Syntax csomópontként reprezentál. A leggyakoribb típusok: DeclSyntax (deklarációk), ExprSyntax (kifejezések), StmtSyntax (utasítások), TypeSyntax (típusok). Mindegyik konkrét altípusra as(...) hívással kell castolni, például node.as(StructDeclSyntax.self).

A fa bejárására két út van. Strukturált bejáráshoz használj SyntaxVisitor-t, amibe override-olod a megfelelő visit(_:) metódust. Egyszerű lekérdezéshez a memberBlock.members, parameterClause.parameters property-k jól működnek. Új csomópont építésre szintén két út van: a builder DSL (mint a fenti ExtensionDeclSyntax { ... }), vagy a nyers "... \(raw: ...)" string interpoláció, amit a fordító a fordítás során parse-ol vissza AST-vé.

A makró segéd-API-ról és a beépített típusokról hivatalos referencia található az Apple SwiftSyntaxMacros dokumentációban. Mivel a SwiftSyntax egy hatalmas API felület (több ezer csomópont típus), érdemes az AST Explorer-t és a generált DocC-t együtt használni.

Diagnosztikák és fordítási hibák kezelése

Egy jó makró nem csak akkor működik jól, ha a bemenet helyes, hanem akkor is, ha rossz. Ha valaki @AutoEquatable-t tesz egy class-ra, ahol nem értelmezett az érték-szemantika, a makró ne crash-eljen, hanem adjon érthető hibaüzenetet. Erre szolgál a MacroExpansionContext.diagnose(_:):

import SwiftDiagnostics

enum AutoEquatableError: String, DiagnosticMessage {
    case onlyOnStruct
    var message: String {
        switch self {
        case .onlyOnStruct: return "@AutoEquatable csak struct típusokon használható."
        }
    }
    var diagnosticID: MessageID { .init(domain: "MyMacros", id: rawValue) }
    var severity: DiagnosticSeverity { .error }
}

// expansion(...)-ben:
guard declaration.is(StructDeclSyntax.self) else {
    context.diagnose(Diagnostic(node: node, message: AutoEquatableError.onlyOnStruct))
    return []
}

Az így bemutatott hiba pontosan ott jelenik meg az Xcode-ban, ahol a node mutat. Vagyis a fejlesztő a makró attributumon látja a piros hullámvonalat, nem valami értelmezhetetlen helyen. A FixIt objektummal javaslatot is mellékelhetsz; ez különösen szeretetreméltó UX, amit a SwiftData makrók is használnak.

Hogyan teszteld a makródat?

A makró tesztelés szövegszinten történik: bemenetként megadod a forráskódot, kimenetként az elvárt kibővített forráskódot, és a tesztkönyvtár összehasonlítja a kettőt. A standard eszköz a SwiftSyntaxMacrosTestSupport modulból érkező assertMacroExpansion:

import XCTest
import SwiftSyntaxMacrosTestSupport
import MyMacrosPlugin

final class AutoEquatableTests: XCTestCase {
    func testStructExpansion() throws {
        assertMacroExpansion(
            """
            @AutoEquatable
            struct User {
                let id: Int
                let name: String
            }
            """,
            expandedSource: """
            struct User {
                let id: Int
                let name: String
            }

            extension User: Equatable {
                public static func == (lhs: User, rhs: User) -> Bool {
                    return lhs.id == rhs.id && lhs.name == rhs.name
                }
            }
            """,
            macros: ["AutoEquatable": AutoEquatableMacro.self]
        )
    }
}

Az indentáció pontosan kell, hogy egyezzen. Egyetlen extra szóköz is buktatja a tesztet, ezért érdemes mindig először lefuttatni a tesztet, vágni az aktuális kimenetet, és azt visszaragasztani expanded oldalra. Ez gyors visszacsatolási hurok. A Swift Testing keretrendszerrel is működik a támogatás, de XCTest-szel kiforrottabb.

Gyakori hibák és teljesítmény tippek

A makró fejlesztés első hónapjában mindenki ugyanazokba a falakba szalad bele. A leggyakoribb: "Macro implementation could not be loaded". Ez majdnem mindig a SwiftSyntax verzió és a fordító verzió közti eltérés miatt van. Frissítsd a from: verziót, futtass swift package reset-et, majd swift build. Második leggyakoribb: a generált kód nem fordul. Ilyenkor jobb klikk a makró hívásra Xcode-ban, majd "Expand Macro", és nézd meg, mi került oda.

Teljesítmény szempontból két dolgot tarts észben. Először, a makró expansion minden fordításnál lefut, ezért ha valami nagyon drága (pl. regex-elemzés egy hosszú stringen), érdemes lazy lenni. Másodszor, ha SwiftUI projektben sok @Observable-t használsz, az inkrementális fordítás látványosan lassulhat. Apple a Swift 6.1-ben már bevezetett egy plugin-caching mechanizmust, ami ezt enyhíti, de a hatás még mindig mérhető. Ha sokat dolgozol a SwiftData adatperzisztenciával, érdemes a debug build-eknél figyelni a "compile time" pontszámokra.

A makró fejlesztés iteratív munka: ritkán működik elsőre, de minden iteráció gyors. A kulcs a kicsi lépésekben dolgozz filozófia: írj egy szerepkört, írd hozzá a tesztet, futtasd, javítsd, csak utána lépj a következőre.

Gyakran ismételt kérdések

Mi a különbség a Swift macro és a C preprocessor makró között?

A Swift macro AST-szinten dolgozik és a generált kód típusellenőrzött, míg a C preprocessor szöveges helyettesítést végez típus-tudás nélkül. Ennek köszönhetően a Swift makrók biztonságosabbak, és az IDE meg tudja mutatni a kibővített eredményt.

Lehet-e debug-olni egy Swift makrót?

Igen, az Xcode-ban beállíthatsz breakpointot a makró pluginban, ha az "Edit Scheme → Run → Executable" alatt a swift-compiler-plugin processzt választod. Egyszerűbb módszer: print hívások a plugin kódban; a kimenet a build log-ban jelenik meg.

Miért nem töltődik be a makróm Xcode 26 alatt?

Az Xcode 26 biztonsági okokból nem futtat aláíratlan plugin-t első alkalommal: a Build hibalistájában megjelenik egy "Trust & Enable Plugin" gomb, amit meg kell nyomni. CI környezetben a defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES paranccsal lehet ezt megkerülni.

Hány makrót használjak egy projektben?

Annyit, amennyi indokolt, de nem több. Saját tapasztalatom: ha egy makró kevesebb mint 3 helyen használt, valószínűleg túl korai absztrakció. A standard @Observable és @Model használata viszont általában mindig megéri.

Tud-e egy makró egy másik fájlt olvasni?

Nem. A makró pluginok sandboxolt folyamatban futnak, és nem férnek hozzá a fájlrendszerhez, hálózathoz vagy környezeti változókhoz. Ez szándékos biztonsági döntés: a fordítás determinisztikus és reprodukálható kell legyen.

Lukas Müller
A Szerzőről Lukas Müller

iOS developer and Swift author since the Objective-C days. Spends his evenings on side projects and his mornings on SwiftUI internals.