Swift Macros: Vodič za izradu prilagođenih makroa u Swiftu 6

Naučite kako napisati vlastite Swift makroe od nule. Vodič pokriva @freestanding i @attached uloge, SwiftSyntax, dijagnostike, testiranje i kada makroe koristiti u Swiftu 6.

Swift Macros Vodič: Swift 6 (2026)

Ažurirano: 15. lipnja 2026.

Swift makroi su mehanizam metaprogramiranja uveden u Swiftu 5.9 i dodatno proširen u Swiftu 6 koji omogućuje generiranje koda u vrijeme prevođenja koristeći SwiftSyntax stablo. Za razliku od C i Objective-C makroa koje pamtim iz davnih vremena, Swift makroi su tipovno sigurni, hermetično izolirani u zasebnim plugin procesima i nikada ne diraju globalni imenski prostor. U ovom vodiču pokazat ću kako napisati vlastiti makro od nule, kada ga koristiti i kako izbjeći zamke s kojima sam se osobno susreo (ima ih više nego što biste očekivali).

  • Swift makroi se izvršavaju u zasebnom plugin procesu u vrijeme prevođenja i generiraju SwiftSyntax stablo, ne tekstualne zamjene.
  • Postoje dvije osnovne kategorije: @freestanding makroi koji se pozivaju s # i @attached makroi koji se primjenjuju kao atributi.
  • Svaki makro deklaracija živi u glavnom modulu, dok implementacija živi u zasebnom CompilerPlugin targetu.
  • Za testiranje makroa Apple nudi službeni SwiftSyntaxMacrosTestSupport modul s assertMacroExpansion funkcijom.
  • Xcode 16 ima ugrađenu opciju "Expand Macro" koja prikazuje konačan generirani kod (koristite je obilno tijekom razvoja).
  • Makroi nisu zamjena za generičke funkcije ili protokole; biraju se kada vam treba boilerplate koji ne može biti izražen kroz tip sustav.

Što su Swift makroi i kako rade?

Swift makro je deklaracija koja prevoditelju kaže da određeni izraz ili deklaracija treba biti zamijenjena generiranim Swift kodom. Generiranje se događa u izoliranom procesu, tzv. compiler plugin-u, koji prima sintaksno stablo ulaza, transformira ga i vraća novo stablo natrag prevoditelju. Cijeli proces je čisto sintaksni: makro ne zna tip varijable koju vidi, niti može mijenjati stanje izvan svog ulaza.

Ovaj dizajn je svjesna reakcija na bol koju su mnogi od nas trpjeli s #define u Objective-C-u. Tekstualni preprocesor je bio moćan, ali nepredvidljiv. Makro je mogao tiho promijeniti značenje koda kilometrima od mjesta gdje je definiran. Swift makroi su, naprotiv, tipovno provjereni nakon ekspanzije, što znači da prevoditelj odbacuje svaki makro koji generira nevažeći kod, s preciznom dijagnostikom koja pokazuje točan red.

Cijela infrastruktura počiva na swift-syntax biblioteci, koju Apple razvija kao otvoreni izvor. Vaš makro ne manipulira stringovima, on radi s tipiziranim čvorovima poput FunctionDeclSyntax, VariableDeclSyntax ili ClosureExprSyntax. Učenje SwiftSyntaxa zato je veći dio puta nego učenje samog Macro API-ja.

Vrste makroa: freestanding vs attached

Swift razlikuje dvije velike obitelji makroa. Freestanding makroi se pozivaju kao izraz ili deklaracija prefiksirana znakom #. Najpoznatiji primjeri iz standardne biblioteke su #warning(...), #unavailable(...) i Apple-ovi #URL("...") i #Predicate iz Foundation-a. Attached makroi se pišu kao atributi (@Observable, @Model, @AddCompletionHandler) i primijenjuju se na deklaraciju ispod njih.

Unutar svake obitelji postoje uloge koje određuju što makro smije generirati:

UlogaObiteljŠto generiraTipičan primjer
@freestanding(expression)freestandingjedan Swift izraz#stringify(x + y)
@freestanding(declaration)freestandingjednu ili više deklaracija#warning("TODO")
@attached(peer)attachednove deklaracije pored ulazneasinkrona inačica funkcije
@attached(member)attachednove članove unutar tipa@Observable dodaje pohranu
@attached(accessor)attachedget/set blokove na svojstvu@UserDefault("key")
@attached(memberAttribute)attachedatribute na članovima tipaoznačavanje svih svojstava s @Published
@attached(extension)attachedextension koji dodaje protokol konformaciju@Codable implementacija

U praksi ćete često kombinirati više uloga na istom makrou. Klasičan primjer je @Observable koji koristi member, memberAttribute i extension uloge istovremeno. Svaka uloga zahtijeva odgovarajući protokol u implementaciji (MemberMacro, AccessorMacro, ExtensionMacro itd.), a propustiti deklarirati ulogu znači da prevoditelj odbija ekspanziju i prije nego što je vaš plugin pozvan.

Postavljanje Swift Package projekta za makro

Makro mora živjeti u Swift paketu. Xcode 16 projekti ne mogu sami hostati implementaciju makroa jer mu treba poseban .macro target type. Najlakši način da krenete je swift package init --type macro u terminalu, što generira pripremljenu strukturu. Ako gradite ručno, evo minimalnog Package.swift koji koristim za sve svoje makro projekte:

// 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.git", 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"),
            ]
        ),
    ]
)

Bitno je razumjeti odnos triju targeta. .macro target je plugin proces koji prevoditelj pokreće tijekom kompilacije. .target je obična biblioteka koja sadrži deklaracije makroa, to je ono što korisnici importaju. .testTarget koristi posebni test support modul jer testiranje makroa traži pristup sirovom plugin procesu, a ne biblioteci. Bez ova tri targeta, makro se neće graditi.

Implementacija vlastitog makroa korak po korak

Napišimo praktičan makro koji rješava stvaran problem: #URL("...") verziju koja se prevodi u compile error ako je URL string nevažeć. Ovaj makro štedi vam ! i prebacuje rizik s runtimea na compile time.

Prvo deklaracija u glavnom MyMacros targetu:

import Foundation

@freestanding(expression)
public macro URL(_ value: String) -> URL = #externalMacro(
    module: "MyMacrosPlugin",
    type: "URLMacro"
)

Atribut #externalMacro govori prevoditelju gdje pronaći implementaciju. Tip URL je obični Foundation tip, a makro samo proizvodi izraz koji se njemu evaluira. Sad implementacija u plugin targetu:

import SwiftSyntax
import SwiftSyntaxMacros
import SwiftCompilerPlugin

public struct URLMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        guard let argument = node.arguments.first?.expression,
              let stringLiteral = argument.as(StringLiteralExprSyntax.self),
              stringLiteral.segments.count == 1,
              case .stringSegment(let segment) = stringLiteral.segments.first
        else {
            throw MacroError.requiresStaticString
        }

        let urlString = segment.content.text
        guard let _ = URL(string: urlString) else {
            throw MacroError.invalidURL(urlString)
        }

        return "URL(string: \(literal: urlString))!"
    }
}

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

Tri stvari koje vrijedi naglasiti. Prvo, makro radi samo s literalima (pokušaj proslijediti varijablu rezultirat će dijagnostikom u vrijeme prevođenja, što je upravo ono što želimo). Drugo, koristim string interpolaciju s \(literal: ...) da generiram sigurno enkodiran string literal natrag u kod. Treće, ! u izlaznom kodu je siguran jer smo već validirali URL u našem makrou, pa runtime ekvivalent ne može propasti.

Ovaj pristup metaprogramiranja sjedi negdje između onoga što sam pokrivao u vodiču za Swift Concurrency i tehnika koje koristi Observation framework s @Observable makroom. Oba primjera pokazuju kako prevoditelj generira boilerplate koji bi inače krasio svaki vaš tip.

Dijagnostike i validacija ulaznih argumenata

Bacanje Error-a iz makroa pretvara se u generičku poruku prevoditelja. Za pravu izvedbu, onu koja se osjeća kao dio standardne biblioteke, koristite context.diagnose(...) s DiagnosticMessage protokolom. To vam daje boju, naslov, ID za fix-it sugestiju i, što je najvažnije, ispravnu lokaciju u izvornom kodu.

import SwiftDiagnostics

struct InvalidURLDiagnostic: DiagnosticMessage {
    let urlString: String
    var message: String { "'\(urlString)' nije važeći URL" }
    var diagnosticID: MessageID {
        MessageID(domain: "MyMacros", id: "invalidURL")
    }
    var severity: DiagnosticSeverity { .error }
}

// Unutar expansion(...):
let diagnostic = Diagnostic(
    node: argument,
    message: InvalidURLDiagnostic(urlString: urlString)
)
context.diagnose(diagnostic)
return "URL(string: \(literal: urlString))" // fallback bez !

Za @attached makroe imate dodatni alat: FixItMessage, koji Xcode pretvara u clickable "Fix" gumb. To je razlika između makroa koji se osjeća kao igračka i onoga koji se osjeća kao da je oduvijek bio dio jezika.

Kada koristiti Swift makroe, a kada ne?

Makroi su moćni, ali nisu prvi alat za kojim bih posegnuo. Moja interna lista za odluku izgleda ovako:

  1. Može li to riješiti generička funkcija? Ako da, koristite je. Generike su čitljivije, debugiraju se trivijalno i prijatelj su autocomplete-a.
  2. Može li to riješiti protokol s default implementacijom? Protokoli su izrazno snažni za sve gdje je oblik koda isti, ali ponašanje različito po tipu.
  3. Generira li svaki korisnik istih 30+ linija boilerplate-a? Sad razgovaramo o makrou. Klasični primjeri su Codable sinteza (koju Swift ima ugrađenu), validacija URL-ova, generiranje CaseIterable varijanti, mock objekti za testove.
  4. Treba li mi compile-time sigurnost izvan onoga što tip sustav nudi? Validacija regularnih izraza, SQL upita, JSON shema. Sve su to legitimni slučajevi.

Ono što me osobno najviše impresioniralo u Swiftu 6 je kako su Apple-ovi framework timovi razdvojili jezičnu mehaniku od framework funkcionalnosti. SwiftData bi bio nemoguć bez makroa: @Model generira tridesetak skrivenih svojstava, dvije ekstenzije i koordinaciju s persistent containerom, sve iz jedne linije atributa.

Debugiranje i testiranje makroa u Xcode 16

Dvije tehnike koje su mi postale neizostavne. Prva: u Xcode 16 desnim klikom na poziv makroa imate "Expand Macro" opciju koja otvara generirani kod u zasebnoj editor traci. To je primarni način da provjerite radi li makro što mislite. Ne print, ne breakpointi unutar plugin procesa.

Druga: unit testovi koristeći assertMacroExpansion. Honestly, ovo je najlakši dio cijelog ekosustava:

import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import MyMacrosPlugin

final class URLMacroTests: XCTestCase {
    func testValidURL() throws {
        assertMacroExpansion(
            #"#URL("https://swift.org")"#,
            expandedSource: #"URL(string: "https://swift.org")!"#,
            macros: ["URL": URLMacro.self]
        )
    }

    func testInvalidURLProducesDiagnostic() throws {
        assertMacroExpansion(
            #"#URL("ne valja")"#,
            expandedSource: #"URL(string: "ne valja")"#,
            diagnostics: [
                DiagnosticSpec(
                    message: "'ne valja' nije važeći URL",
                    line: 1, column: 1
                )
            ],
            macros: ["URL": URLMacro.self]
        )
    }
}

Testovi se pokreću trenutno jer ne trebaju cijeli Xcode build pipeline, radite izravno sa SwiftSyntax stablom. Ovaj pristup je u skladu s onim što sam pokrivao u vodiču za Swift Testing, iako sami makro testovi za sada ostaju na XCTest-u jer test support modul još nije migriran.

Često postavljana pitanja

Koja je razlika između Swift makroa i C preprocesor makroa?

Swift makroi rade na tipiziranom SwiftSyntax stablu, izvršavaju se u izoliranom procesu i njihov izlaz prolazi punu provjeru tipova. C makroi su tekstualne zamjene koje se događaju prije parsiranja, što ih čini moćnima ali nepredvidljivima. Swift makro ne može mijenjati semantiku van svog ulaza, dok C #define može.

Mogu li Swift makroi pristupiti informacijama o tipu?

Ne mogu. Makroi rade isključivo na sintaksnoj razini i izvršavaju se prije analize tipova. Ako vaš makro mora znati tip izraza, jedini način je da korisnik tu informaciju eksplicitno proslijedi kao argument ili da makro generira kod koji odgađa odluku na runtime kroz type(of:).

Usporavaju li makroi vrijeme prevođenja?

Da, posebno na prvom clean buildu. Plugin proces se mora pokrenuti i SwiftSyntax stablo serijalizirati. U mojim mjerenjima, makro težak projekt vidi 1.5x do 2x duže clean build vrijeme, ali inkrementalni buildovi ostaju brzi zbog Xcode keširanja ekspanzija.

Kako objaviti Swift makro paket na Swift Package Index?

Označite Package.swift s swift-tools-version: 5.9 ili novijim, dodajte tagiranu verziju (npr. 1.0.0) na Git repozitorij i predajte URL na swiftpackageindex.com. Nije potrebno posebno konfigurirati, index prepoznaje .macro targete automatski i prikazuje ih u listi proizvoda.

Rade li Swift makroi na svim Apple platformama?

Rade na iOS 13+, macOS 10.15+, watchOS 6+ i tvOS 13+ jer ekspanzija se odvija u vrijeme prevođenja, a ne runtime. Sam Swift toolchain za izgradnju makroa zahtijeva Swift 5.9 ili noviji, dostupan u Xcode 15 i kasnijim verzijama. Aplikacije izgrađene s makroima ne nose runtime ovisnost.

Lukas Müller
O Autoru 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.