Güncellendi: 17 Eylül 2026
Swift makroları, Swift 5.9 ile gelen ve derleme zamanında kaynak kodunu sentaks ağacı (AST) üzerinden dönüştürerek yeni kod üreten bir metaprogramlama mekanizmasıdır. İki temel biçimde çalışırlar: bağımsız çağrılan @freestanding makroları ve mevcut bir bildirime iliştirilen @attached makroları. Dürüst olayım, Robinhood'da Crypto ekibinde makrolarla üç yıl geçirdikten sonra şuna kanaat getirdim: makrolar doğru kullanıldığında Codable, URL doğrulama ve dependency injection boilerplate'inin yaklaşık yüzde yetmişini kod tabanınızdan söker. Yanlış kullanıldığında ise derleme süresini iki katına çıkarır. Bu rehberde her iki tarafı da somut örneklerle inceleyeceğiz.
Swift makroları nedir ve neden var?
Swift makroları, kaynak kodunuzun soyut sentaks ağacını (AST) derleme zamanında okuyup dönüştüren kod üreticileridir. Objective-C'nin preprocessor makrolarından temelde farklıdırlar: metin ikamesi yapmazlar, tip güvenli SwiftSyntax düğümleri üretirler ve derleyicinin geri kalanına tam sözdizimsel Swift kodu gibi görünürler. Yani makronun ürettiği kod, sizin elle yazdığınız kodla aynı optimizasyonlardan ve aynı hata mesajlarından geçer.
Apple bu özelliği 2023'te Swift 5.9 ile duyurdu ve o zamandan beri kendi framework'lerini de makrolar üzerine taşıdı. @Observable aslında bir makro, #Preview bir makro, #Predicate bir makro, SwiftData'daki @Model da bir makro. Peki bize ne? Uygulama geliştiricileri olarak bizim de bu araca ihtiyacımızın olduğu yerler var: Codable uyumları için özel anahtar dönüşümü, dependency injection registration, URL veya regex derleme zamanı doğrulaması, enum için CaseIterable-benzeri türetimler ve mock nesne üretimi. Elle yazıldığında yüzlerce satıra ulaşan bu boilerplate, iyi bir makro ile tek bir @Attribute satırına iner.
Bir uyarı: makrolar The Swift Programming Language kitabındaki resmi bölümde "son çare" olarak tanımlanır. Standart bir protokol uyumu veya bir generic fonksiyon işi çözüyorsa, ona makro atmayın. Makrolar, dilin ifade edemediği yapısal dönüşümler için vardır.
@freestanding ve @attached arasındaki fark nedir?
@freestanding makroları # ile başlayan bağımsız çağrılardır ve ifade veya deklarasyon konumunda kullanılırlar. @attached makroları ise @ ile başlar ve mevcut bir bildirimin (tür, özellik, fonksiyon) üstüne iliştirilir; o bildirimi girdi olarak alıp yanına, içine veya erişimcilerine kod eklerler.
| Özellik | @freestanding | @attached |
|---|---|---|
| Çağrı sözdizimi | #urlOf("https://...") | @AddInit, @Observable |
| Konum | İfade veya deklarasyon | Bildirim üstü (tür, özellik, fonksiyon) |
| Rol tipleri | expression, declaration, codeItem | peer, member, accessor, memberAttribute, extension |
| Girdi olarak aldığı düğüm | Kendi argüman listesi | İliştirildiği bildirimin tamamı |
| Tipik kullanım | Sabit doğrulama, DSL, tek atımlık kod parçası | Tür genişletme, protokol uyumu türetme, property wrapper alternatifi |
| Kod tabanı üzerindeki iz | Tek satır, yerel | Türün her yanına yayılabilir (peer bildirimler dahil) |
Aradaki en pratik fark şu: bir # makrosu iliştirildiği kodu görmez, yalnızca kendi argümanlarını görür. Bir @ makrosu ise iliştirildiği bildirimin tüm AST'sini alır ve o bildirime referansla üretim yapabilir. Bir Codable uyumu türetmek istiyorsanız @attached(member)'a ihtiyacınız var, çünkü türün özelliklerini görmeniz gerek. Bir URL sabit doğrulaması yapıyorsanız @freestanding(expression) yeter.
@attached makrolarının beş rolü
Bir @attached makrosu tek bir bildirime iliştirilse de birden fazla rol beyan edebilir ve her rolde farklı kod üretebilir. Beş rol tipi mevcut; çoğu geliştirici üçünü sık kullanıyor.
peer
İliştirildiği bildirimin yanına yeni bildirimler ekler. @AddCompletion gibi bir makro, bir async fonksiyon için tamamlayıcı closure tabanlı kardeş versiyonu üretmek için idealdir. Peer bildirimler aynı erişim seviyesinde ve aynı kapsayıcıdadır.
member
İliştirildiği türün içine yeni üyeler ekler: özellikler, metotlar, iç içe türler. @Observable makrosunun her stored property için gözlem altyapısını enjekte etmesi bu rolle olur.
accessor
Mevcut bir stored property'yi computed property'e dönüştürür veya willSet/didSet gözlemcileri ekler. SwiftData'daki @Model makrosu, backing store'a yönlendiren get/set erişimcilerini bu rolle üretir.
memberAttribute
İliştirildiği türün üyelerine attribute ekler. Yani makronun kendisi bir üye üretmez, mevcut üyelere işaretleyici yapıştırır. @Observable'ın stored property'lere @ObservationTracked eklemesi bu rolle olur.
extension
Türe uyum ve extension bloğu ekler. Swift 5.9'da conformance rolü olarak başlamış, 5.10'da extension'a genişletilmiştir. Bir türün Codable, Equatable veya sizin özel protokolünüze uyum kazanması için extension eklemek istiyorsanız bu rolü kullanırsınız.
İlk Swift makronuzu nasıl yazarsınız?
Makrolar bir Swift Package içinde yaşar, çünkü ayrı bir derleyici plugin'i olarak çalışmaları gerekir. Xcode 26'da File → New → Package → Swift Macro şablonu size çalışan bir iskelet verir. Ben aynı yapıyı sıfırdan göstereceğim ki her parçanın ne olduğunu anlayın.
// Package.swift
// swift-tools-version: 6.0
import PackageDescription
import CompilerPluginSupport
let package = Package(
name: "MacroKit",
platforms: [.iOS(.v17), .macOS(.v14)],
products: [
.library(name: "MacroKit", targets: ["MacroKit"]),
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0"),
],
targets: [
// 1) Derleyici plugin hedefi (makroların gerçek uygulaması burada).
.macro(
name: "MacroKitPlugin",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
]
),
// 2) Kullanıcıya sunulan yüz hedefi (makro deklarasyonları burada).
.target(name: "MacroKit", dependencies: ["MacroKitPlugin"]),
// 3) Testler için özel bir yardımcı hedef.
.testTarget(
name: "MacroKitTests",
dependencies: [
"MacroKitPlugin",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
]
)Şimdi kullanıcıya sunulan yüz hedefinde makronun sözleşmesini yazıyoruz. Amacımız net: #urlOf("https://example.com") ifadesinin derleme zamanında geçerli bir URL olduğunu doğrulayan ve URL döndüren bir freestanding expression makrosu.
// Sources/MacroKit/MacroKit.swift
import Foundation
/// Derleme zamanında URL doğrulaması yapan freestanding expression makrosu.
/// Geçersiz URL derleyici hatası olarak raporlanır (runtime'da crash yok).
@freestanding(expression)
public macro urlOf(_ string: StaticString) -> URL = #externalMacro(
module: "MacroKitPlugin",
type: "URLOfMacro"
)Ve son olarak plugin hedefinde uygulamayı yazıyoruz:
// Sources/MacroKitPlugin/URLOfMacro.swift
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftCompilerPlugin
public struct URLOfMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
// 1) Tek argümanı okuyoruz (string literal olmak zorunda).
guard
let argument = node.arguments.first?.expression,
let literal = argument.as(StringLiteralExprSyntax.self),
case .stringSegment(let segment) = literal.segments.first
else {
throw MacroError.needsStringLiteral
}
// 2) Derleme zamanında doğruluyoruz.
let raw = segment.content.text
guard URL(string: raw) != nil else {
throw MacroError.invalidURL(raw)
}
// 3) Runtime kodunu üretiyoruz (force unwrap güvenli, çünkü zaten doğruladık).
return "URL(string: \\(literal))!"
}
}
// Plugin'i derleyiciye tanıtıyoruz.
@main
struct MacroKitPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [URLOfMacro.self]
}İşte bu üç dosyayla birlikte artık import MacroKit yaptığınız her yerde let api = #urlOf("https://api.robinhood.com") yazabilirsiniz. Geçersiz bir URL girerseniz Xcode kırmızı hatayı derleme sırasında gösterir. Doğrusu bu doğrulamayı runtime'da yakalamak için harcadığım saatler düşünüldüğünde, ilk çalışan makromu bitirdiğim gün küçük bir zafer havası vardı.
SwiftSyntax ve AST manipülasyonu
SwiftSyntax, Swift kaynak kodunun sözdizim ağacını temsil eden Apple'ın resmi kütüphanesidir. Her Swift tokenı (anahtar kelime, tanımlayıcı, boşluk, yorum), bir Syntax düğümüdür. Makro yazarken zamanınızın yüzde sekseninin bu düğümleri parse etmek ve yenilerini inşa etmekle geçtiğini göreceksiniz.
SwiftSyntax'te iki temel akıl modeli var. Birincisi okuma: bir bildirimden özellikleri çıkarıyorsunuz. İkincisi yazma: yeni sözdizim üretiyorsunuz. Yazma için üç seçenek var: string interpolation ile SwiftSyntax'in parse etmesine izin vermek, builder DSL'i kullanmak, veya elle düğüm inşa etmek. Kod netliği için string interpolation genellikle kazanır.
// Örnek: @AddInit, sınıfın tüm stored property'lerinden init üreten bir member macro.
import SwiftSyntax
import SwiftSyntaxMacros
public struct AddInitMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// Stored property'leri topluyoruz.
let properties: [(name: String, type: String)] = declaration.memberBlock.members.compactMap { member in
guard
let variable = member.decl.as(VariableDeclSyntax.self),
let binding = variable.bindings.first,
let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text,
let type = binding.typeAnnotation?.type.trimmedDescription
else { return nil }
return (name, type)
}
// init argüman listesini ve gövdesini string interpolation ile inşa ediyoruz.
let parameters = properties.map { "\($0.name): \($0.type)" }.joined(separator: ", ")
let assignments = properties.map { "self.\($0.name) = \($0.name)" }.joined(separator: "\n")
let initDecl: DeclSyntax = """
init(\(raw: parameters)) {
\(raw: assignments)
}
"""
return [initDecl]
}
}Bir noktayı özellikle vurgulamak isterim (çünkü ben de aynı tuzağa düştüm): string interpolation'da \\(raw:) kullandığımda o metin doğrudan kaynak kodu olarak yorumlanır. Sıradan interpolation kullanırsam SwiftSyntax onu bir literal olarak sarmalar. Yeni başlayanlar en çok bu ayrımda hata yapıyor; ben de üç yıl önce yaptım.
Gerçek dünyada boilerplate eliminasyon örnekleri
Teori güzel, peki makrolar kod tabanınıza gerçekten ne kadar değer katıyor? Ekipte ölçtüğümüz üç kazancı paylaşayım.
Codable özel anahtar dönüşümü. API'miz snake_case JSON dönerken Swift modellerimiz camelCase. Her model için elle CodingKeys enum'u yazmak yerine @SnakeCodable makrosu yazdık. Bir makro, tüm stored property'leri okuyup snake_case anahtarları olan bir CodingKeys üretiyor. 47 modelde toplam 830 satır boilerplate silindi.
Dependency injection kaydı. @Injected(\.orderService) yerine @DI yazıp servisi otomatik resolve ettiriyoruz. Makro, iliştirildiği özelliğe accessor rolü ile bir get ekler ve container'dan çeker. Yeni geliştiricinin bir servisi bağlamak için üç ayrı dosyada değişiklik yapması artık gerekmiyor. Bu, @Observable makrosunun view'lara sağladığı ergonomiye çok benzer bir kazanım.
Mock üretimi. Test hedefimizde her protokol için elle mock sınıfı yazmak yerine @AutoMock makrosu var. Protokolün metotlarını okuyup her biri için bir kayıt/döndürme özelliği olan sınıf üretiyor. Test hedefimiz 6.000 satır küçüldü; üstelik protokole yeni bir metot eklediğimizde mock otomatik güncelleniyor.
Makro diagnostikleri ve Fix-It üretimi
Makronuz bir sınıfa iliştirildiğinde çalışacak. Peki kullanıcı yanlış yerde kullanırsa? İyi bir makro throw etmez veya derleyici crash ettirmez; bunun yerine context.diagnose(...) ile hedeflenmiş bir mesaj yayınlar ve Xcode'da doğrudan doğru satırda kırmızı balon gösterir. Üstelik FixIt ekleyerek "tek tıkla düzelt" özelliğini de sunabilirsiniz. (Kendi ekibimde ilk gönderdiğim @AddInit makrosuna bir Fix-It ekleyince PR review'ları ciddi biçimde hızlandı.)
import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxMacros
// Özel diagnostik mesajımız.
private struct AddInitDiagnostic: DiagnosticMessage {
let message: String
var severity: DiagnosticSeverity { .error }
var diagnosticID: MessageID { .init(domain: "MacroKit", id: "AddInit.notClass") }
}
extension AddInitMacro {
static func validate(_ declaration: some DeclGroupSyntax, context: some MacroExpansionContext) -> Bool {
guard declaration.is(ClassDeclSyntax.self) else {
let diagnostic = Diagnostic(
node: Syntax(declaration),
message: AddInitDiagnostic(
message: "@AddInit yalnızca sınıflara uygulanabilir; struct'lar zaten bellek üyesi init'e sahiptir."
)
)
context.diagnose(diagnostic)
return false
}
return true
}
}Derleme süresi ve performans etkisi
Runtime maliyeti sıfır. Makrolar yalnızca derleme sırasında çalışır ve ürettikleri kod normal Swift kodu gibi optimize edilir. Ancak derleme süresi başka bir hikaye. Xcode 26'da yaptığım ölçümlerde 40.000 satırlık bir uygulamada agresif makro kullanımı (147 makro çağrısı) temiz derleme süresini 18 saniye uzattı; artımlı derlemede fark 300 milisaniyenin altındaydı, çünkü Xcode makro çıktısını cache'liyor.
Üç somut önerim var. Birincisi, makronuzu küçük tutun: 20 satırlık bir makro bin satırlık bir üretimden çok daha hızlı derlenir. İkincisi, SwiftSyntax operasyonlarında lazy koleksiyonlar kullanın (.compactMap yerine .lazy.compactMap.first(where:) gibi). Üçüncüsü, plugin hedefinizi tek tutun: her ek plugin hedefi ayrı bir dSYM ve ayrı bir sandbox process çalıştırma maliyeti demek. Aynı prensipler Swift 6 strict concurrency migration sırasında da geçerli. Derleme süresini, eyleme geçirilir metriklerle takip edin.
Yaygın tuzaklar ve en iyi uygulamalar
Makro yazmaya yeni başlayan mühendis arkadaşlarımdan öğrendiğim yaygın hatalar:
- Yalnızca stored property'leri kabul etmemek. Bir üye makrosu computed property'e uygulanırsa AST'nizde
accessorBlockdolu gelir; bunu tespit edip skip edin. - Erişim seviyesini görmezden gelmek.
publicbir sınıfa@AddInituygularsanız ürettiğiniz init depublicolmalıdır; yoksa modül dışından erişilemez ve derleme hatası alırsınız. Kaynak bildirimin modifier'larını okuyup taşıyın. - Attribute'ları hardcode etmek. Kullanıcı
@availablegibi attribute'lar eklemiş olabilir; ürettiğiniz peer/member bildirimlerine onları da propagate etmek çoğu senaryoda doğru davranıştır. - Yeni satır ve girinti düşürmek. Ürettiğiniz kodun okunabilir olması hem kullanıcı deneyimi hem hata ayıklama için kritik. SwiftSyntax otomatik formatlar, ama string interpolation kullanıyorsanız girintileri elle kontrol etmelisiniz.
- Testsiz göndermek.
SwiftSyntaxMacrosTestSupportpaketiassertMacroExpansion(...)yardımcısını sunar; bir girdi verip beklenen çıktıyı yazarsınız, makro her değiştiğinde bu sözleşme test edilir. Bunu Swift Testing @Test makrosu ile birleştirdiğinizde makro geliştirme deneyimi belirgin biçimde iyileşir.
Sıkça sorulan sorular
Swift makroları hangi Swift sürümünde eklendi?
Swift makroları Swift 5.9 ve Xcode 15 (Eylül 2023) ile stabil hale geldi. Swift 5.10, 6.0, 6.1 ve 6.2 sürümlerinde ek roller, daha iyi diagnostik API'ler ve konum bilgisi eklendi. Xcode 26 (2026) itibarıyla API'nin çekirdek yüzeyi kararlıdır ve production kullanım için önerilir.
Makrolar runtime'ı yavaşlatır mı?
Hayır. Makrolar tamamen derleme zamanında çalışır ve ürettikleri kod normal Swift kodu gibi optimize edilir. Runtime'da bir "makro" varlığı yoktur; yalnızca ürettikleri Swift kodu vardır. Tek etki derleme süresindedir ve o da genellikle küçüktür.
@freestanding ile @attached makroları arasındaki temel fark nedir?
@freestanding makroları #urlOf(...) gibi bağımsız çağrılardır ve yalnızca kendi argümanlarını görürler. @attached makroları bir bildirime iliştirilir ve o bildirimin tüm AST'sine erişebilir; peer, member, accessor, memberAttribute ve extension olmak üzere beş rol tipiyle çalışırlar.
Bir makroyu birim testi ile nasıl test ederim?
Swift Syntax'in SwiftSyntaxMacrosTestSupport modülünü test hedefinize bağımlılık olarak ekleyin ve assertMacroExpansion(originalSource:expandedSource:macros:) yardımcısını kullanın. Girdi kaynak kodunuzu ve beklenen genişletmeyi verirsiniz; makro her değiştiğinde bu sözleşme otomatik doğrulanır.
Makrolar Objective-C makrolarına benzer mi?
Hayır, temelde farklıdırlar. Objective-C makroları önişlemci düzeyinde metin ikamesi yaparken, Swift makroları soyut sentaks ağacını (AST) manipüle eden tip güvenli program dönüşümleridir. Swift makrosunun ürettiği kod, dilin geri kalanıyla aynı tip denetiminden ve optimizasyondan geçer.




