Swift Makrá v Swift 6.2: Sprievodca @attached a #freestanding makrami
Praktický sprievodca tvorbou vlastných Swift makier. Od freestanding cez attached až po sedem rolí, SwiftSyntax, testovanie a debugovanie v Xcode 16.4.
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.
Swift makrá nahrádzajú väčšinu prípadov, kde sme v Objective-C siahali po preprocesore alebo runtime reflexii.
Existujú dve rodiny: freestanding (volané ako #myMacro) a attached (anotácie typu @MyMacro so siedmimi rolami).
Makrá sa implementujú v samostatnom Swift balíku ako compiler plugin a pracujú nad AST cez knižnicu swift-syntax.
Xcode 16 ponúka Expand Macro aj krokovanie do generovaného kódu, takže makrá nie sú čierna skrinka.
Testovanie sa robí cez SwiftSyntaxMacrosTestSupport a porovnáva vstupný a očakávaný výstupný zdrojový text.
Štandardná knižnica už dnes obsahuje makrá ako @Observable, #Predicate, #URL a v Swift 6.2 pribúdajú ďalšie.
Č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.
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/didSet pre property.
memberAttribute: pripne ďalšie atribúty na členy typu (napríklad @ObservationTracked na každú stored property).
extension: pridá conformance a extension blok (nahrádza staršiu conformance rolu).
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:
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:
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:
#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.
@CasePathable z 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.
Ako pridať haptickú spätnú väzbu v SwiftUI cez SensoryFeedback a Core Haptics. Praktický sprievodca s hodnotami intenzity, AHAP súbormi a príkladmi z produkcie pre iOS 26.
Naučte sa stavať Live Activities a Dynamic Island v iOS 26 cez ActivityKit, App Intents a APNs. Praktické ukážky kódu pre lock screen, StandBy a Smart Stack na watchOS 26.
Kompletný sprievodca prístupnosťou v SwiftUI pre iOS 26. Pokrýva VoiceOver, Dynamic Type, @ScaledMetric, accessibility traits, Reduce Motion, AccessibilityRotor a audio grafy v Swift Charts. S príkladmi kódu a praktickými tipmi z code review.