Aktualizované: 12. júna 2026
Swift makrá sú metaprogramovací mechanizmus zavedený v Swift 5.9, ktorý umožňuje generovať a transformovať zdrojový kód v čase kompilácie pomocou SwiftSyntax. Na rozdiel od C-čkových makier zdedených z čias Objective-C sú swiftovské makrá typovo bezpečné, hygienické a bežia v izolovanom kompilátorovom plugine. V tomto sprievodcovi prejdeme od základných pojmov cez tvorbu vlastných @attached a #freestanding makier až po ich testovanie a ladenie v Xcode 16.4.
Čo sú Swift makrá a prečo vznikli
Swift makrá sú deklarácie, ktoré pri kompilácii pretransformujú zdrojový kód na základe vstupných argumentov a anotovaného kontextu. Boli oficiálne prijaté cez návrhy SE-0382 (Expression Macros) a SE-0389 (Attached Macros), a stabilne dostupné sú od Swift 5.9 (Xcode 15). V Swift 6.2 (Xcode 16.4) sú už mainstreamovou súčasťou jazyka.
Z pohľadu vývojára, ktorý začínal v Objective-C ére, je rozdiel oproti starému preprocesoru zásadný. #define v C bol slepý textový substituent: nepoznal typy, nemal hygienické scope-y a hlásil chyby až po expanzii. Swift makrá naopak dostávajú typovo overený AST, expanzia musí produkovať syntakticky validný Swift a všetky chyby sa hlásia presne nad volaním makra, nie kdesi v generovanom kóde. Tým konečne dostávame metaprogramovanie, ktoré nepôsobí ako zub bolesti.
Konkrétne problémy, ktoré makrá riešia, sa pohybujú od jednoduchých kompilačných konštánt (napríklad #URL("https://...") validuje URL už pri builde) cez generovanie boilerplate kódu (@Observable dopĺňa getter/setter pre každú property) až po doménovo špecifické DSL-éčka, ako je #Predicate v SwiftData query API.
Aký je rozdiel medzi freestanding a attached makrami?
Freestanding makrá voláte explicitne so znakom # a stoja samé, buď ako výraz, alebo ako deklarácia. Attached makrá pripájate k existujúcej deklarácii pomocou @ a vstupujú do jej štruktúry. Toto je hlavné rozhodnutie pri návrhu vlastného makra: čo má byť vstupom?
| Vlastnosť | Freestanding (#) | Attached (@) |
|---|---|---|
| Syntax volania | #myMacro(args) | @MyMacro nad deklaráciou |
| Typické použitie | literály, výrazy, asserty | generovanie členov, conformance, accessorov |
| Roly | expression, declaration | peer, member, accessor, memberAttribute, extension, conformance, body |
| Vidí kontext deklarácie? | nie | áno, celý DeclSyntax |
| Príklad zo stdlib | #Predicate, #URL, #expect | @Observable, @Model, @Test |
| Hygiena identifikátorov | úplná | úplná, no roly ako member môžu pridať nové mená |
Praktické pravidlo. Ak generujete jednorazovú hodnotu alebo overujete literál, siahnite po freestanding. Ak chcete „dovymyslieť" typ (synthesovať Codable, dať mu publikujúce property wrappery, prepísať telo funkcie), siahnite po attached. V Swift 6.2 navyše pribudla rola body, ktorá umožňuje nahradiť celé telo funkcie. To je užitočné pre logging, profiling a tracing bez runtime swizzlingu, ktorý sme v Obj-C ére zneužívali až prihusto.
Ako vytvoriť vlastné makro v Swift krok za krokom
Začnime jednoduchým freestanding makrom #stringify, ktoré vráti dvojicu (hodnota, jej zdrojový text). Je to kanonický príklad z Apple dokumentácie. Štruktúra projektu je vždy rovnaká: jeden Swift Package s tromi cieľmi.
// Package.swift
// 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", from: "601.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"),
]
),
]
)
Deklarácia samotného makra patrí do verejnej knižnice MyMacros:
// Sources/MyMacros/Stringify.swift
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) =
#externalMacro(module: "MyMacrosPlugin", type: "StringifyMacro")
A implementácia v plugin cieli:
// Sources/MyMacrosPlugin/StringifyMacro.swift
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxMacros
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]
}
Použitie v aplikácii potom vyzerá nenápadne:
import MyMacros
let (value, code) = #stringify(2 + 3)
print(value, code) // 5 "2 + 3"
Všimnite si tri detaily. Makro je typed (vracia (T, String)), expanzia stavia ExprSyntax cez string interpoláciu, čo nie je textový hack: kompilátor parsuje výsledok a interpolácia \(literal: ...) z neho urobí korektne escapovaný stringový literál. A nakoniec, implementácia neimportuje Foundation, pretože beží v plugin procese, nie v cieľovom kóde.
Sedem rolí attached makier v praxi
Attached makrá majú v Swift 6.2 sedem rolí, ktoré určujú, čo makro do deklarácie pridáva. Často sa kombinujú. Typický @Observable používa member aj memberAttribute aj extension naraz.
- peer: pridá súrodenú deklaráciu vedľa anotovanej (napríklad async variant funkcie).
- member: pridá nové členy do typu (properties, metódy, init).
- accessor: dorobí
get/set/willSet/didSetpre property. - memberAttribute: pripne ďalšie atribúty na členy typu (napríklad
@ObservationTrackedna každú stored property). - extension: pridá conformance a extension blok (nahrádza staršiu
conformancerolu). - body (Swift 6.2+): nahradí alebo wrapne telo funkcie či inicializátora.
- preamble (návrh SE-0468, experiment v 6.2): vloží kód na začiatok funkcie bez nahradzovania tela.
Ukážka praktickej kombinácie. Predstavte si @AutoLogged, ktoré dopĺňa logovanie pred a po každom volaní funkcie:
@attached(body)
public macro AutoLogged() =
#externalMacro(module: "MyMacrosPlugin", type: "AutoLoggedMacro")
// Použitie:
struct PaymentService {
@AutoLogged
func charge(amount: Decimal) async throws -> Receipt {
try await gateway.submit(amount)
}
}
Po expanzii kompilátor vidí ekvivalent funkcie, ktorá si pred volaním zapíše vstupné argumenty cez os.Logger a po návrate čas trvania. V Objective-C ére sme to riešili method swizzlingom, runtime hackom, ktorý sa ťažko debugoval. Honestly, prvýkrát keď som @AutoLogged štýl nasadil do payment pipeline, ušetril mi to celý víkend hľadania, kde sa „stratil" jeden retry. Makro expanduje deterministicky, v čase buildu, a Xcode vám ukáže presne, čo sa vygenerovalo.
Čo je SwiftSyntax a ako s ním pracovať
SwiftSyntax je oficiálna knižnica od Apple, ktorá reprezentuje Swift zdrojový kód ako typovaný strom (Concrete Syntax Tree). Žije v repozitári swift-syntax na GitHube a každá major verzia (501, 600, 601…) zodpovedá verzii Swift kompilátora. Pri vývoji makier ju budete používať trojakým spôsobom.
1. Čítanie vstupu. Z node alebo declaration argumentu vyberáte konkrétne uzly:
guard let structDecl = declaration.as(StructDeclSyntax.self) else {
throw MacroError.notAStruct
}
let storedProperties = structDecl.memberBlock.members
.compactMap { $0.decl.as(VariableDeclSyntax.self) }
.filter { $0.bindings.first?.accessorBlock == nil }
2. Validácia. Cez context.diagnose(...) hlásite kompilátoru chyby s presným umiestnením. Žiadne kryptické „undefined symbol", používateľ uvidí červenú vlnovku presne nad chybnou property.
3. Generovanie výstupu. Buď cez string interpoláciu (ExprSyntax/DeclSyntax sú StringLiteralConvertible), alebo cez builder API StructDeclSyntax(...) { MemberBlockItemListSyntax { ... } }. String forma je čitateľnejšia pre menšie makrá, builder lepšie odolá zmenám v gramatike.
Testovanie makier cez SwiftSyntaxMacrosTestSupport
Makro je čistá funkcia (vstupný AST) → (výstupný AST), takže sa testuje krásne. Apple poskytuje SwiftSyntaxMacrosTestSupport s helperom assertMacroExpansion, ktorý porovnáva textovú reprezentáciu pred a po expanzii a zlyhá s pekným diffom.
import SwiftSyntaxMacrosTestSupport
import XCTest
final class StringifyMacroTests: XCTestCase {
let macros: [String: Macro.Type] = ["stringify": StringifyMacro.self]
func testBasicExpression() {
assertMacroExpansion(
"""
let result = #stringify(a + b)
""",
expandedSource: """
let result = (a + b, "a + b")
""",
macros: macros
)
}
func testDiagnostic() {
assertMacroExpansion(
"let bad = #stringify()",
expandedSource: "let bad = #stringify()",
diagnostics: [
DiagnosticSpec(message: "missing argument", line: 1, column: 11)
],
macros: macros
)
}
}
Ak ste si prešli náš sprievodcu @Observable a Observation frameworkom, narazili ste presne na ten typ makra, ktorého testy tu kopírujeme. V CI odporúčam mať pre každé makro aspoň jeden test pozitívny (úspešná expanzia) a jeden test diagnostiky (zachytenie chyby). Som už dvakrát zachránil PR vďaka tomu, že diagnostický test odhalil regresiu po bump-e swift-syntax.
Ako debugovať Swift makrá v Xcode
Najčastejšia otázka začiatočníkov: „moje makro generuje niečo zlé, ako zistím čo?" Xcode 16 ponúka tri nástroje, ktoré používam každý deň.
- Expand Macro. Right-click na volanie makra a zvoľte Expand Macro. Otvorí sa inline editor s výslednou expanziou. Funguje pre attached aj freestanding.
- Step Into Macro Expansion. Pri debugovaní môžete krokovať priamo do generovaného kódu. Xcode má pre expanziu vygenerovaný virtuálny súbor.
- Compiler diagnostics. Ak makro emituje
context.diagnose(...), chyba sa zobrazí ako natívna kompilačná chyba vrátane fix-itov, ak ich pripravíte.
Pre development cyklus odporúčam mať testy makra v samostatnej schéme a spúšťať swift test --filter z príkazového riadku, je to rýchlejšie než plný build hostiteľského projektu. Keď narazíte na opakujúci sa pád pluginu, zapnite SWIFT_MACRO_PLUGIN_VERBOSE=1 a uvidíte stderr z plugin procesu v build logu Xcode. Pri záhadných pádoch je vinníkom najčastejšie zlá verzia swift-syntax oproti toolchainu (sám som tu strávil pár hodín, kým som si na to spomenul).
Vstavané makrá v Swift 6.2 a iOS 26
Predtým, než si napíšete vlastné, oplatí sa vedieť, čo už ponúka štandardná knižnica a Apple frameworky. Oficiálna swift.org dokumentácia makier ich zoznam priebežne aktualizuje:
@Observable: generuje Observation tracking pre triedy, podrobne v našom sprievodcovi @Observable v SwiftUI.@Model: SwiftData persistence boilerplate.#Predicate,#Expression: typovo bezpečné dotazy pre SwiftData a Core Data.#URL: compile-time validovaná URL (od iOS 17).@Test,#expect,#require: Swift Testing framework.@CasePathablez Point-Free knižnice: populárna externá ukážka, ako masívne ušetriť boilerplate pre enumy.@Entry(SwiftUI v iOS 18+): definuje environment value jediným riadkom.@Generable(Foundation Models v iOS 26): označí typ ako vstup pre on-device LLM s odvodenou JSON schémou.
Trend je jasný. Apple postupne prepisuje časti SwiftUI a Foundation z protocol witnessov a property wrapperov na makrá. Property wrappery boli kompromis ranej éry Swiftu; makrá ich postupne nahrádzajú, lebo umožňujú generovať aj typovú conformance, nielen wrapper okolo hodnoty. Ak vás zaujíma, ako sa to spája s novou súbežnosťou, pozrite si náš text o súbežnosti v Swift 6.2, kde makrá hrajú kľúčovú rolu pri generovaní typovaných error mapingov.
Sú Swift makrá bezpečné a aké majú obmedzenia?
Áno, Swift makrá sú výrazne bezpečnejšie než C makrá, ale majú niekoľko praktických obmedzení, ktoré sa oplatí poznať. Implementácie bežia v izolovanom plugin procese, ktorý kompilátor spúšťa cez sandbox. Plugin nemá sieť, nemá zápis do filesystému mimo build adresára a nemá prístup k environment premenným cieľového procesu. To znamená, že makro nemôže do vašej appky prepašovať škodlivý kód v runtime, všetko, čo robí, sa zhmotní v zdrojáku a uvidíte to v Expand Macro.
Praktické obmedzenia. Makrá nemôžu spúšťať váš kód v čase kompilácie (žiadne „evaluuj funkciu"). Pracujú výlučne so syntaxou, nepoznajú typový kontext, nemajú prístup k tabuľke symbolov a nevedia rozhodnúť, či je Int v kontexte naozaj Swift.Int. Tomu sa hovorí type-checking after expansion. Pri návrhu DSL-éčiek (ako #Predicate) treba s tým rátať: generujete kostru a typový check sa udeje až po nej.
Ďalšie, čo treba vedieť. Makrá zvyšujú build time (každý plugin sa kompiluje raz a potom spúšťa pre každú expanziu). V Xcode 16.4 sú expanzie cachované per súbor, čo masívne pomohlo. Pri verejných knižniciach makrá vyžadujú, aby spotrebiteľ schválil plugin pri prvom builde (dialóg Trust & Enable): bezpečnostná poistka, ktorá ostro kontrastuje s tichou inštaláciou skriptov závislostí z minulosti.
Často kladené otázky
Od ktorej verzie Swift sú makrá dostupné?
Stabilne od Swift 5.9 (Xcode 15, jeseň 2023). Pre produkčné použitie v iOS aplikáciách potrebujete deployment target iOS 17+, ak chcete vstavané makrá ako #URL alebo @Observable. Vlastné makrá fungujú aj pre staršie targety, pretože expandujú na bežný Swift kód.
Môžem v makre čítať súbor alebo volať API?
Nie. Makrá bežia v sandboxovanom procese bez prístupu k filesystému a sieti. Je to úmyselné, zabraňuje to tomu, aby plugin počas buildu tajne sťahoval závislosti alebo telemetroval. Ak potrebujete generovať kód z externých zdrojov, použite Swift Package plugin (BuildToolPlugin) namiesto makra.
Aký je rozdiel medzi makrami a property wrapperami?
Property wrapper je runtime mechanizmus. Wrappuje hodnotu, no nemení typ, neumožňuje pridať conformance ani vygenerovať nové členy. Makro je build-time transformácia, ktorá vie všetko spomenuté a navyše nemá runtime overhead. V novom kóde uprednostnite makrá; property wrappery zostávajú vhodné pre jednoduché stavové wrappery (@State, @Binding).
Spomalia makrá kompiláciu?
Mierne áno. Plugin sa musí postaviť (raz na konfiguráciu) a každá expanzia stojí pár milisekúnd. V projekte s tisíckami volaní makier to môže byť 1 až 3 sekundy navyše pri clean buildoch. Pri inkrementálnych buildoch sú expanzie cachované od Xcode 16.
Ako verzionovať a distribuovať vlastné makro?
Distribuujte ako Swift Package s .macro targetom. V Package.swift pripnite swift-syntax cez .upToNextMajor(from: "601.0.0") a v CI testujte proti aktuálnemu Swift toolchainu. Pri major verziách kompilátora (Swift 7 a vyššie) očakávajte, že budete musieť bumpnúť major verziu swift-syntax.




