Swift Macros: Kompletní průvodce vytvářením vlastních maker pro Swift 6 (2026)

Praktický průvodce vývojem vlastních Swift Macros ve Swift 6 a Xcode 16. Projdeme freestanding i attached makra, nastavení SwiftPM projektu, diagnostiku chyb i testování s assertMacroExpansion. Včetně funkčních ukázek kódu pro #stringify a @DictionaryStorage.

Swift Macros: Vlastní makra Swift 6 (2026)

Aktualizováno: 3. června 2026

Swift Macros jsou kompilační rozšíření jazyka Swift, která umožňují generovat opakující se kód v době kompilace bez runtime overheadu a se zachováním typové bezpečnosti. Představují bezpečnou alternativu k preprocesorovým makrům z C/Objective-C a jsou implementovány jako samostatné kompilátorové pluginy postavené nad knihovnou SwiftSyntax. V tomto průvodci pro Swift 6 (Xcode 16+) si projdeme oba typy maker, tedy @freestanding a @attached, a vytvoříme si vlastní funkční makra od nuly.

  • Swift Macros generují kód v době kompilace pomocí samostatných kompilátorových pluginů a balíčku swift-syntax od Applu.
  • Existují dva druhy: @freestanding (volaná přes #nazev) a @attached (aplikovaná jako anotace @Nazev na deklarace).
  • Attached makra mají pět rolí: member, peer, accessor, extension a memberAttribute.
  • Implementace makra žije v samostatném SwiftPM targetu typu .macro a běží jako sandboxovaný proces oddělený od vašeho kódu.
  • Testování maker probíhá pomocí assertMacroExpansion z modulu SwiftSyntaxMacrosTestSupport. Žádný runtime, jen porovnávání vyexpandovaného zdrojového textu.
  • Pro diagnostiku chyb se používají typy implementující DiagnosticMessage a metody context.diagnose(...).

Co jsou Swift Macros a proč je používat?

Swift Macros byly oficiálně představeny ve Swiftu 5.9 (Xcode 15) a od Swift 6 se staly stabilní součástí jazyka, na které stojí klíčové frameworky Applu jako @Observable, #Preview, @AddAsync v testech nebo nově @Entry ve SwiftUI. Jejich smyslem je odstranit boilerplate, který by jinak musel vývojář psát ručně. Typicky jde o implementace protokolů, syntetizaci inicializátorů nebo opakované volání metod s podobnými parametry.

Na rozdíl od preprocesorových maker v C jsou Swift Macros plně typově bezpečná. Kompilátor je nespouští jako textovou náhradu, ale jako transformaci AST (Abstract Syntax Tree). Zdrojový kód je nejprve naparsován knihovnou apple/swift-syntax do struktury SyntaxNode, makro vrátí nový kus syntaxe a kompilátor jej vloží zpět do překládaného souboru. Výsledný kód je viditelný v Xcode (pravým tlačítkem na makro a "Expand Macro"), takže ladění je velmi přímočaré.

Hlavní výhody oproti runtime reflexi (Mirror) nebo dynamickým generátorům kódu jsou tři. Nulový runtime overhead (vše se vyřeší při kompilaci), úplná typová kontrola (kompilátor odmítne nevalidní makro stejně jako jakýkoli jiný kód) a transparentnost (vyexpandovaný kód můžete kdykoli zobrazit a debugovat krok po kroku). Upřímně, když jsem poprvé migroval starší ObservableObject kód na novou syntaxi v produkční aplikaci, byl to právě tenhle "Expand Macro" pohled, co mi ušetřilo hodiny tápání. Pokud řešíte stejnou migraci, mrkněte na náš samostatný článek o migraci na @Observable makro ve SwiftUI.

Typy maker: freestanding vs. attached

Swift rozlišuje dva základní druhy maker podle toho, jak se volají v cílovém kódu. Freestanding makra se používají s prefixem # a vystupují jako samostatné výrazy nebo deklarace. Typickým příkladem je #Preview { ContentView() } ve SwiftUI nebo #warning("TODO"). Attached makra se naopak aplikují jako anotace začínající @ na konkrétní deklaraci (třídu, strukturu, vlastnost, funkci) a transformují její chování.

Freestanding makra mají dvě podoby. @freestanding(expression) generuje výraz (např. #stringify(x + y) vrátí tuple (value, source)) a @freestanding(declaration) generuje jednu či více deklarací (např. #myEnum může vygenerovat celý enum). Volání freestanding makra je vždy explicitní a v místě volání je jasné, že se jedná o generovaný kód.

Attached makra jsou výrazově silnější a mají pět rolí, které lze libovolně kombinovat:

  • @attached(member): přidává nové členy (properties, metody, inicializátory) do typu.
  • @attached(peer): přidává sourozenecké deklarace vedle té, na kterou je makro napojeno.
  • @attached(accessor): přidává getter/setter/willSet/didSet k vlastnosti.
  • @attached(extension): generuje extension včetně konformancí s protokoly.
  • @attached(memberAttribute): přidává atributy ke každému členu typu (např. @Published automaticky).

Reálné Apple makro @Observable kombinuje role member, memberAttribute a extension. Proto z obyčejné třídy udělá sledovaný observable typ jednou anotací.

Nastavení projektu pro vývoj makra

Makro nemůže žít přímo ve vašem aplikačním kódu. Musí být v samostatném SwiftPM targetu typu .macro, protože ho kompilátor spouští jako externí proces v sandboxu. Doporučený postup je vytvořit balíček příkazem swift package init --type macro, který vygeneruje šablonu se třemi targety: implementací makra, knihovnou pro public deklaraci a testovacím targetem.

Minimální Package.swift vypadá takto:

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

let package = Package(
    name: "MyMacros",
    platforms: [.macOS(.v13), .iOS(.v17)],
    products: [
        .library(name: "MyMacros", targets: ["MyMacros"]),
    ],
    dependencies: [
        .package(url: "https://github.com/swiftlang/swift-syntax.git",
                 from: "600.0.0"),
    ],
    targets: [
        // Implementacni target, bezi jako kompilatorovy plugin
        .macro(
            name: "MyMacrosImpl",
            dependencies: [
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
            ]
        ),
        // Public knihovna, kterou importuji uzivatele
        .target(name: "MyMacros", dependencies: ["MyMacrosImpl"]),
        // Testy
        .testTarget(
            name: "MyMacrosTests",
            dependencies: [
                "MyMacrosImpl",
                .product(name: "SwiftSyntaxMacrosTestSupport",
                         package: "swift-syntax"),
            ]
        ),
    ]
)

První freestanding makro krok za krokem

Začněme klasickým příkladem #stringify. Jde o makro, které vezme libovolný výraz a vrátí tuple s jeho hodnotou a textovou reprezentací zdrojového kódu. Public deklarace v cílové knihovně MyMacros:

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

Klíčové slovo #externalMacro propojuje veřejnou deklaraci s implementací v plugin targetu. Sama implementace v MyMacrosImpl vypadá takto:

// Sources/MyMacrosImpl/StringifyMacro.swift
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftCompilerPlugin

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

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

Všimněte si, že vracíme ExprSyntax sestavený pomocí string interpolation. SwiftSyntax umožňuje takto skládat AST přímo z textu, ale výsledek je typově kontrolovaný. Použití v aplikaci je pak triviální:

import MyMacros

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

Attached makro pro generování členů

Tak, a teď něco reálnějšího. Vyzkoušíme si makro @DictionaryStorage, které z vlastností struktury vytvoří getter/setter ukládající hodnoty do interního slovníku. Tahle technika se hodí pro lehké datové wrapery nebo pro implementaci typed property listů.

// Public deklarace
@attached(accessor)
public macro DictionaryStorage() =
    #externalMacro(module: "MyMacrosImpl", type: "DictionaryStorageMacro")

Implementace vrací pole AccessorDeclSyntax, které kompilátor vloží do těla vlastnosti:

import SwiftSyntax
import SwiftSyntaxMacros

public struct DictionaryStorageMacro: AccessorMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingAccessorsOf declaration: some DeclSyntaxProtocol,
        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)?.identifier
        else {
            return []
        }
        let key = "\"\(identifier.text)\""
        return [
            """
            get { storage[\(raw: key)] as! Self.Type.PropertyType }
            """,
            """
            set { storage[\(raw: key)] = newValue }
            """,
        ]
    }
}

Použití pak vypadá přirozeně. Uživatel jen označí vlastnost atributem:

struct Settings {
    var storage: [String: Any] = [:]

    @DictionaryStorage var username: String
    @DictionaryStorage var notificationsEnabled: Bool
}

Stejnou techniku můžete kombinovat s @attached(member) pro syntetizaci celých inicializátorů nebo s @attached(extension) pro automatickou konformanci s protokoly jako Codable či Equatable. Pokud chcete jít hlouběji do generování thread-safe kódu, doporučujeme náš článek o bezpečnosti dat ve Swift 6 (Sendable, aktory, Mutex).

Diagnostika chyb a varování v makrech

Solidní makro musí umět uživateli sdělit, že ho použil špatně, třeba na nesprávném typu deklarace. K tomu slouží MacroExpansionContext.diagnose(...) a vlastní typy implementující protokol DiagnosticMessage. Diagnostika se objeví v Xcode přímo na řádku s makrem, stejně jako jakákoli kompilační chyba.

import SwiftDiagnostics

struct NotAStruct: DiagnosticMessage {
    let message = "@DictionaryStorage lze pouzit pouze na strukturach"
    let diagnosticID = MessageID(domain: "MyMacros", id: "notAStruct")
    let severity: DiagnosticSeverity = .error
}

// V expansion(of:in:):
if !(declaration.is(StructDeclSyntax.self)) {
    context.diagnose(Diagnostic(node: node, message: NotAStruct()))
    return []
}

Jak testovat Swift Macros?

Testování maker je překvapivě jednoduché, protože nevyžaduje žádný runtime. Jen porovnáváme očekávaný výstup expanze s tím, co makro skutečně vygenerovalo. Apple k tomu dodává modul SwiftSyntaxMacrosTestSupport s funkcí assertMacroExpansion:

import XCTest
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
@testable import MyMacrosImpl

final class StringifyMacroTests: XCTestCase {
    let testMacros: [String: Macro.Type] = [
        "stringify": StringifyMacro.self,
    ]

    func testStringifyExpansion() {
        assertMacroExpansion(
            """
            let x = #stringify(2 + 3)
            """,
            expandedSource: """
            let x = (2 + 3, "2 + 3")
            """,
            macros: testMacros
        )
    }
}

Pokud vaše makro generuje diagnostiku, můžete ji ověřit pomocí parametru diagnostics:. Tento přístup je deterministický, rychlý a integruje se přímo do běžného XCTest workflow. Tipy pro psaní moderních testů včetně nového frameworku Swift Testing najdete v našem průvodci Swift Testing pro Xcode 16+.

Nejčastější chyby a jak je řešit

Začátečníci nejčastěji narážejí na tři problémy. Prvním je "External macro implementation type 'X' could not be found", což je obvykle způsobeno tím, že název typu v #externalMacro(type: "...") neodpovídá skutečnému jménu struktury v plugin targetu, nebo target není označen jako .macro v Package.swift. (Já osobně tuhle chybu schytal hlavně po refaktoru, kdy jsem přejmenoval strukturu a zapomněl na string v deklaraci.)

Druhým je "The macro 'X' must be declared public". Všechny veřejné deklarace makra musí být public, protože jsou volány z jiných modulů. Implementační typ v plugin targetu naopak public být musí kvůli registraci v CompilerPlugin.

Třetím je vágní syntax error po expanzi. Xcode často hlásí chybu na řádku s makrem, ale skutečný problém je v generovaném kódu. Řešení? Pravým tlačítkem na makro, "Expand Macro" zobrazí výsledek a chybu uvidíte v reálném kontextu. Detailnější popis vývojového procesu najdete v oficiální dokumentaci Swift Macros a v návrhu SE-0382, který definuje sémantiku expression maker.

Pokud nasazujete makra v kódu využívajícím nový model souběžnosti, prostudujte si také Approachable Concurrency ve Swift 6.2. Některé generované struktury totiž potřebují explicitní @MainActor izolaci, aby fungovaly korektně.

Často kladené otázky

Jaký je rozdíl mezi freestanding a attached makrem?

Freestanding makra se volají s prefixem # (např. #Preview) a vystupují jako samostatné výrazy nebo deklarace. Attached makra se aplikují jako anotace s prefixem @ (např. @Observable) na konkrétní typ, vlastnost nebo funkci a transformují její chování přidáním členů, accessorů nebo extensionů.

Mohou Swift Macros nahradit Sourcery nebo jiné generátory kódu?

Pro většinu případů ano. Makra běží přímo v kompilátoru, takže nepotřebují build phase script, mají úplnou typovou kontrolu a jejich výstup je viditelný v Xcode. Sourcery má smysl pouze v projektech, které musejí podporovat starší toolchainy než Swift 5.9.

Zvyšují makra dobu kompilace?

Mírně ano, protože kompilátor musí spustit externí plugin proces. Pro běžná makra je dopad zanedbatelný (jednotky milisekund na expanzi). Inkrementální build cachuje výsledky, takže opakované sestavení nepřináší další náklady.

Jak zobrazím vyexpandovaný kód makra v Xcode?

V editoru klikněte pravým tlačítkem na název makra (např. @Observable) a vyberte "Expand Macro". Otevře se postranní panel s konečnou podobou kódu, který kompilátor skutečně vidí. Tohle je klíčový nástroj při ladění vlastních maker.

Lze v makru použít síťová volání nebo přístup k souborům?

Ne. Plugin běží v sandboxu se striktně omezenými právy: žádný file system, žádná síť, žádné prostředí. Veškerá data, která makro potřebuje, musí přijít přes argumenty nebo skrz analýzu okolní syntaxe. Toto omezení je bezpečnostní funkce a nelze jej obejít.

Editorial Team
O Autorovi Editorial Team

Our team of expert writers and editors.