Swift Makroları Rehberi 2026: @attached, @freestanding ve Kod Üretimi Sanatı

Swift 5.9 ile gelen makrolar, @freestanding ve @attached olmak üzere iki biçimde çalışır. SwiftSyntax ile ilk makronuzu yazacak, diagnostik ve Fix-It üretecek, gerçek boilerplate eliminasyon örneklerini ve derleme süresi etkisini göreceksiniz.

Swift Makroları 2026: @attached & @freestanding
On this page
  1. Swift makroları nedir ve neden var?
  2. @freestanding ve @attached arasındaki fark nedir?
  3. @attached makrolarının beş rolü
  4. İlk Swift makronuzu nasıl yazarsınız?
  5. SwiftSyntax ve AST manipülasyonu
  6. Gerçek dünyada boilerplate eliminasyon örnekleri
  7. Makro diagnostikleri ve Fix-It üretimi
  8. Derleme süresi ve performans etkisi
  9. Yaygın tuzaklar ve en iyi uygulamalar
  10. Sıkça sorulan sorular

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 deklarasyonBildirim üstü (tür, özellik, fonksiyon)
Rol tipleriexpression, declaration, codeItempeer, member, accessor, memberAttribute, extension
Girdi olarak aldığı düğümKendi argüman listesiİliştirildiği bildirimin tamamı
Tipik kullanımSabit doğrulama, DSL, tek atımlık kod parçasıTür genişletme, protokol uyumu türetme, property wrapper alternatifi
Kod tabanı üzerindeki izTek satır, yerelTü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:

  1. Yalnızca stored property'leri kabul etmemek. Bir üye makrosu computed property'e uygulanırsa AST'nizde accessorBlock dolu gelir; bunu tespit edip skip edin.
  2. Erişim seviyesini görmezden gelmek. public bir sınıfa @AddInit uygularsanız ürettiğiniz init de public olmalı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.
  3. Attribute'ları hardcode etmek. Kullanıcı @available gibi attribute'lar eklemiş olabilir; ürettiğiniz peer/member bildirimlerine onları da propagate etmek çoğu senaryoda doğru davranıştır.
  4. 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.
  5. Testsiz göndermek. SwiftSyntaxMacrosTestSupport paketi assertMacroExpansion(...) 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.