# Swift Macros: pratik metaprogramlama örnekleri > Swift Macros için kapsamlı rehber: freestanding ve attached makro oluşturma, swift-syntax ile AST manipülasyonu ve tekrar eden kodu ortadan kaldıran pratik örnekler. - Published: 2026-03-11 - Updated: 2026-05-01 - Author: SharpSkill - Tags: swift, ios, macros, metaprogramming, swift-syntax - Reading time: 16 min --- Swift 5.9 ve Xcode 15 ile gelen Swift Macros, Swift kodunun yazılma biçiminde bir devrim niteliği taşır. Bu özellik, derleme sırasında kod üretilmesini sağlayarak boilerplate'i ortadan kaldırırken statik tip güvenliğini korur. C ön işlemcisi makrolarının aksine Swift Macros tip güvenli, derleyiciye entegre ve geliştirme araçları tarafından tam olarak desteklenmektedir. > **Bu rehberin kapsamı** > > Bu rehber Swift Macros oluşturmayı baştan sona ele alır: temel kavramlardan ileri seviye uygulamalara kadar, herhangi bir iOS projesinde kullanılmaya hazır çalışan kod örnekleriyle. ## Swift Macros türlerini anlamak Swift, her biri farklı kullanım senaryolarına hizmet eden iki ana makro kategorisi sunar. Freestanding makrolar bağımsız olarak ifade veya bildirim biçiminde çalışırken attached makrolar mevcut bildirimlere bağlanarak onları değiştirir veya zenginleştirir. ### Freestanding makrolar: ifade ve bildirim Freestanding makrolar `#` simgesiyle başlar ve ya bir değer döndürür (ifade) ya da yeni bildirimler oluşturur. İşte somut bir ifade makrosu örneği: ```swift // MacroUsage.swift // Freestanding expression macro - generates a value let buildInfo = #buildDate // Expansion → "2026-03-11 10:30:45" // Freestanding macro with arguments let message = #stringify(1 + 2) // Expansion → "1 + 2 = 3" // Freestanding declaration macro - creates declarations #makeCase("success", "failure", "pending") // Expansion → // case success // case failure // case pending ``` İfade ile bildirim arasındaki temel fark sonuçtadır: ifade bir değer üretir, bildirim ise yapısal kod üretir (tipler, fonksiyonlar, değişkenler). ### Attached makrolar: beş rol Attached makrolar `@` ile başlar ve bir bildirimin önüne yerleştirilir. Swift bu makrolar için beş farklı rol tanımlar: ```swift // AttachedMacroRoles.swift // @attached(peer) - adds declarations at the same level @AddAsync func fetchUser(id: Int) -> User { ... } // Expansion → adds func fetchUserAsync(id: Int) async -> User // @attached(accessor) - adds getters/setters @UserDefault("theme") var currentTheme: String // Expansion → adds get { UserDefaults.standard.string(...) } // @attached(member) - adds members to a type @AutoEquatable struct Point { var x: Int var y: Int } // Expansion → adds static func == (lhs: Point, rhs: Point) -> Bool // @attached(memberAttribute) - applies attributes to members @CodableKeys struct Config { var apiUrl: String var timeout: Int } // Expansion → adds @CodingKey("api_url") before apiUrl // @attached(conformance) / @attached(extension) - adds conformances @Hashable struct User { var id: Int var name: String } // Expansion → adds extension User: Hashable { ... } ``` Bu roller, kodu aynı anda birden fazla boyutta dönüştüren güçlü makrolar oluşturmak için birleştirilebilir. ## Makro oluşturmak için proje kurulumu Swift Macros oluşturmak, belirli bir yapıya sahip bir Swift Package gerektirir. Paket, Swift kodunu soyut sözdizimi ağacı (AST) olarak işleyen resmi kütüphane swift-syntax'a bağlıdır. ### Package.swift yapısı ```swift // Package.swift // swift-tools-version: 5.9 import PackageDescription let package = Package( name: "MyMacros", platforms: [.macOS(.v10_15), .iOS(.v13)], products: [ // Library exposing macros to the main project .library( name: "MyMacros", targets: ["MyMacros"] ), // Executable for testing macros .executable( name: "MyMacrosClient", targets: ["MyMacrosClient"] ) ], dependencies: [ // Required dependency for macros .package( url: "https://github.com/apple/swift-syntax.git", from: "509.0.0" ) ], targets: [ // Compiler plugin containing implementation .macro( name: "MyMacrosPlugin", dependencies: [ .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax") ] ), // Target exposing macro declarations .target( name: "MyMacros", dependencies: ["MyMacrosPlugin"] ), // Test client .executableTarget( name: "MyMacrosClient", dependencies: ["MyMacros"] ), // Unit tests .testTarget( name: "MyMacrosTests", dependencies: [ "MyMacrosPlugin", .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax") ] ) ] ) ``` Bu yapılandırma, makro bildirimlerini (istemci kodun gördüğü bölüm) uygulamadan (derleme zamanında çalışan bölüm) net biçimde ayırır. > **Önerilen düzen** > > En az üç dosya gereklidir: bildirimler için MyMacros.swift, uygulamalar için MyMacrosPlugin.swift ve testler için MyMacrosTests.swift. Bu ayrım bakım sürecini kolaylaştırır. ## İfade makrosu oluşturma İfade makroları kod içinde doğrudan kullanılabilir bir değer üretir. Aşağıda, opsiyonel bir değeri açan ve hata mesajına değişken adını ekleyen `#unwrap` makrosunun nasıl oluşturulacağı gösterilmektedir. ### Makronun bildirimi ```swift // MyMacros.swift import Foundation /// Macro that unwraps an optional with an explicit error message /// Usage: let value = #unwrap(optionalValue) /// Expansion: guard let optionalValue else { fatalError("...") }; optionalValue @freestanding(expression) public macro unwrap(_ value: T?) -> T = #externalMacro( module: "MyMacrosPlugin", type: "UnwrapMacro" ) ``` İmza, makronun bir opsiyonel aldığını ve opsiyonel olmayan değeri döndürdüğünü belirtir. `#externalMacro` plugin içindeki uygulamayı işaret eder. ### swift-syntax ile uygulama ```swift // UnwrapMacro.swift import SwiftSyntax import SwiftSyntaxMacros import SwiftCompilerPlugin public struct UnwrapMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> ExprSyntax { // Get the first argument passed to the macro guard let argument = node.argumentList.first?.expression else { throw MacroError.missingArgument } // Extract the variable name for the error message let variableName = argument.description.trimmingCharacters( in: .whitespacesAndNewlines ) // Generate the expansion code // Uses an immediately-invoked closure to encapsulate the guard return """ { guard let value = \(argument) else { fatalError("Failed to unwrap '\\(\(literal: variableName))' - value was nil") } return value }() """ } } // Custom errors for macros enum MacroError: Error, CustomStringConvertible { case missingArgument case invalidSyntax(String) var description: String { switch self { case .missingArgument: return "The macro requires an argument" case .invalidSyntax(let message): return "Invalid syntax: \(message)" } } } ``` `expansion` metodu, makro çağrısını temsil eden AST düğümünü ve derleme bağlamını alır. Üretilen kodu içeren bir `ExprSyntax` döndürür. ### Plugin kaydı ```swift // MyMacrosPlugin.swift import SwiftCompilerPlugin import SwiftSyntaxMacros @main struct MyMacrosPlugin: CompilerPlugin { // List all macros provided by this plugin let providingMacros: [Macro.Type] = [ UnwrapMacro.self, // Add other macros here ] } ``` Bu giriş noktası, plugin içinde mevcut olan makrolar hakkında derleyiciyi bilgilendirir. ## Attached member makrosu oluşturma Member makroları mevcut bir tipe yeni üyeler (özellikler, metotlar, iç içe tipler) ekler. Aşağıdaki `@AutoInit` makrosu, tüm depolanan özellikleri içeren bir başlatıcıyı otomatik olarak üretir. ### Eksiksiz bildirim ve uygulama ```swift // MyMacros.swift /// Automatically generates an initializer with all stored properties @attached(member, names: named(init)) public macro AutoInit() = #externalMacro( module: "MyMacrosPlugin", type: "AutoInitMacro" ) ``` ```swift // AutoInitMacro.swift import SwiftSyntax import SwiftSyntaxMacros public struct AutoInitMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { // Verify the macro is applied to a struct or class guard declaration.is(StructDeclSyntax.self) || declaration.is(ClassDeclSyntax.self) else { throw MacroError.invalidSyntax( "@AutoInit can only be applied to structs and classes" ) } // Collect stored properties let properties = declaration.memberBlock.members .compactMap { $0.decl.as(VariableDeclSyntax.self) } .filter { isStoredProperty($0) } // Generate initializer parameters let parameters = properties.compactMap { property -> String? in guard let binding = property.bindings.first, let identifier = binding.pattern.as(IdentifierPatternSyntax.self), let type = binding.typeAnnotation?.type else { return nil } let name = identifier.identifier.text let typeName = type.description.trimmingCharacters(in: .whitespaces) // Check if the property has a default value if binding.initializer != nil { return "\(name): \(typeName) = \(binding.initializer!.value)" } return "\(name): \(typeName)" } // Generate assignments in the init body let assignments = properties.compactMap { property -> String? in guard let binding = property.bindings.first, let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else { return nil } let name = identifier.identifier.text return "self.\(name) = \(name)" } // Build the complete initializer let initDecl: DeclSyntax = """ public init(\(raw: parameters.joined(separator: ", "))) { \(raw: assignments.joined(separator: "\n ")) } """ return [initDecl] } // Check if a variable is a stored property (not computed) private static func isStoredProperty(_ variable: VariableDeclSyntax) -> Bool { guard let binding = variable.bindings.first else { return false } // A computed property has an accessor block with get/set if let accessor = binding.accessorBlock { // If it's a block with explicit accessors, it's computed if accessor.accessors.is(AccessorDeclListSyntax.self) { return false } } // let or var without accessor = stored property return true } } ``` ### AutoInit makrosunun kullanımı ```swift // UserModel.swift @AutoInit struct User { let id: UUID var name: String var email: String var isActive: Bool = true } // Automatically generated code: // public init(id: UUID, name: String, email: String, isActive: Bool = true) { // self.id = id // self.name = name // self.email = email // self.isActive = isActive // } // Usage let user = User(id: UUID(), name: "Alice", email: "alice@example.com") // isActive uses the default value ``` Bu makro başlatıcı boilerplate'ini ortadan kaldırır; bu özellikle çok sayıda alana sahip veri modellerinde değerlidir. ## Async üretim için attached peer makrosu Peer makroları, işaretlenen bildirimle aynı seviyede yeni bildirimler ekler. Aşağıda completion handler tabanlı bir fonksiyonun async sürümünü üreten `@AddAsync` makrosu yer alır. ```swift // MyMacros.swift /// Automatically generates an async version of a function with completion handler @attached(peer, names: suffixed(Async)) public macro AddAsync() = #externalMacro( module: "MyMacrosPlugin", type: "AddAsyncMacro" ) ``` ```swift // AddAsyncMacro.swift import SwiftSyntax import SwiftSyntaxMacros public struct AddAsyncMacro: PeerMacro { public static func expansion( of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { // Verify it's a function guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else { throw MacroError.invalidSyntax( "@AddAsync requires a function" ) } let functionName = funcDecl.name.text let asyncFunctionName = "\(functionName)Async" // Analyze parameters to find the completion handler let parameters = funcDecl.signature.parameterClause.parameters // Filter parameters (exclude completion handler) var regularParams: [String] = [] var completionType: String? = nil for param in parameters { let paramType = param.type.description // Detect a completion handler (closure with Result or simple value) if paramType.contains("->") && paramType.contains("Void") { // Extract the return type from completion completionType = extractCompletionReturnType(from: paramType) } else { let paramName = param.firstName.text let paramSecondName = param.secondName?.text let label = paramSecondName ?? paramName regularParams.append("\(paramName): \(paramType)") } } guard let returnType = completionType else { throw MacroError.invalidSyntax( "No completion handler found" ) } // Generate arguments for internal call let callArgs = parameters.dropLast().map { param in let name = param.firstName.text return "\(name): \(name)" }.joined(separator: ", ") // Generate the async function let asyncFunc: DeclSyntax = """ func \(raw: asyncFunctionName)(\(raw: regularParams.joined(separator: ", "))) async throws -> \(raw: returnType) { try await withCheckedThrowingContinuation { continuation in \(raw: functionName)(\(raw: callArgs.isEmpty ? "" : callArgs + ", ")completion: { result in switch result { case .success(let value): continuation.resume(returning: value) case .failure(let error): continuation.resume(throwing: error) } }) } } """ return [asyncFunc] } // Extract return type from a Result type private static func extractCompletionReturnType(from type: String) -> String { // Simplified pattern - in production, use the AST if let match = type.range(of: #"Result<([^,]+)"#, options: .regularExpression) { var result = String(type[match]) result = result.replacingOccurrences(of: "Result<", with: "") return result.trimmingCharacters(in: .whitespaces) } return "Void" } } ``` ### AddAsync makrosunun kullanımı ```swift // NetworkService.swift class NetworkService { @AddAsync func fetchUser( id: Int, completion: @escaping (Result) -> Void ) { // Implementation with callback URLSession.shared.dataTask(with: URL(string: "/users/\(id)")!) { data, _, error in if let error = error { completion(.failure(error)) } else if let data = data { let user = try? JSONDecoder().decode(User.self, from: data) completion(.success(user!)) } }.resume() } // Automatically generates: // func fetchUserAsync(id: Int) async throws -> User { // try await withCheckedThrowingContinuation { continuation in // fetchUser(id: id, completion: { result in // switch result { // case .success(let value): // continuation.resume(returning: value) // case .failure(let error): // continuation.resume(throwing: error) // } // }) // } // } } // Modern usage with async/await let user = try await networkService.fetchUserAsync(id: 42) ``` > **Peer makrolarının sınırları** > > Üretilen fonksiyonun adı, `@attached` özniteliğinin `names:` bölümünde bildirilmelidir. Burada `suffixed(Async)`, üretilen fonksiyonun orijinal adına "Async" sonekinin ekleneceğini belirtir. ## Makrolar için birim testler Makroları test etmek hayati önem taşır çünkü daha sonra derlenecek kodu üretirler. Swift, bu testleri kolaylaştırmak için `SwiftSyntaxMacrosTestSupport` kütüphanesini sunar. ```swift // MyMacrosTests.swift import SwiftSyntaxMacros import SwiftSyntaxMacrosTestSupport import XCTest @testable import MyMacrosPlugin final class MyMacrosTests: XCTestCase { // Dictionary of macros to test let testMacros: [String: Macro.Type] = [ "unwrap": UnwrapMacro.self, "AutoInit": AutoInitMacro.self, "AddAsync": AddAsyncMacro.self ] func testUnwrapMacroExpansion() throws { assertMacroExpansion( """ let value = #unwrap(optionalString) """, expandedSource: """ let value = { guard let value = optionalString else { fatalError("Failed to unwrap 'optionalString' - value was nil") } return value }() """, macros: testMacros ) } func testAutoInitMacroWithStruct() throws { assertMacroExpansion( """ @AutoInit struct Point { let x: Int var y: Int } """, expandedSource: """ struct Point { let x: Int var y: Int public init(x: Int, y: Int) { self.x = x self.y = y } } """, macros: testMacros ) } func testAutoInitWithDefaultValues() throws { assertMacroExpansion( """ @AutoInit struct Config { var timeout: Int = 30 var retryCount: Int } """, expandedSource: """ struct Config { var timeout: Int = 30 var retryCount: Int public init(timeout: Int = 30, retryCount: Int) { self.timeout = timeout self.retryCount = retryCount } } """, macros: testMacros ) } func testAutoInitFailsOnEnum() throws { assertMacroExpansion( """ @AutoInit enum Status { case active } """, expandedSource: """ enum Status { case active } """, diagnostics: [ DiagnosticSpec( message: "@AutoInit can only be applied to structs and classes", line: 1, column: 1 ) ], macros: testMacros ) } } ``` Testler kodun doğru genişletilmesini ve hatalı kullanımda uygun hata mesajlarının gösterilmesini doğrular. ## İleri seviye makro: gözlemlenebilir property wrapper Bu makro, otomatik bildirimli bir özellik gözlem sistemi inşa etmek için birden fazla rolü birleştirir. ```swift // MyMacros.swift /// Adds automatic property change observation @attached(accessor) @attached(peer, names: prefixed(_)) public macro Observable() = #externalMacro( module: "MyMacrosPlugin", type: "ObservableMacro" ) ``` ```swift // ObservableMacro.swift import SwiftSyntax import SwiftSyntaxMacros // Implements both roles: accessor and peer public enum ObservableMacro {} extension ObservableMacro: AccessorMacro { public static func expansion( of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntax, in context: some MacroExpansionContext ) throws -> [AccessorDeclSyntax] { guard let varDecl = declaration.as(VariableDeclSyntax.self), let binding = varDecl.bindings.first, let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else { return [] } let name = identifier.identifier.text let storageName = "_\(name)" // Generate get and set accessors let getter: AccessorDeclSyntax = """ get { access(keyPath: \\.\(raw: name)) return \(raw: storageName) } """ let setter: AccessorDeclSyntax = """ set { withMutation(keyPath: \\.\(raw: name)) { \(raw: storageName) = newValue } } """ return [getter, setter] } } extension ObservableMacro: PeerMacro { public static func expansion( of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { guard let varDecl = declaration.as(VariableDeclSyntax.self), let binding = varDecl.bindings.first, let identifier = binding.pattern.as(IdentifierPatternSyntax.self), let type = binding.typeAnnotation?.type else { return [] } let name = identifier.identifier.text let storageName = "_\(name)" let typeName = type.description // Generate private storage property let initializer = binding.initializer.map { " \($0)" } ?? "" let storageDecl: DeclSyntax = """ private var \(raw: storageName): \(raw: typeName)\(raw: initializer) """ return [storageDecl] } } ``` ### Observable deseninin kullanımı ```swift // ViewModel.swift @Observable class UserViewModel { @Observable var name: String = "" @Observable var age: Int = 0 @Observable var isActive: Bool = true // Generated code for each property: // private var _name: String = "" // var name: String { // get { // access(keyPath: \.name) // return _name // } // set { // withMutation(keyPath: \.name) { // _name = newValue // } // } // } } ``` Apple, Swift 5.9 ile birlikte gelen yeni Observation framework'ünde aynı deseni kullanır. ## Makroları hata ayıklama ve inceleme Xcode, makroların hata ayıklamasında ve üretilen kodu anlamada kullanılabilecek çeşitli araçlar sunar. ### Xcode'da genişletme ```swift // DebuggingMacros.swift // Right-click on macro call → "Expand Macro" // Displays generated code inline @AutoInit struct Product { let id: UUID var name: String var price: Decimal } // To see the expansion: // 1. Right-click on @AutoInit // 2. Select "Expand Macro" // 3. Generated code displays inline for inspection and debugging ``` ### Geliştirme sırasında günlükleme ```swift // DebugMacro.swift public struct DebugMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> ExprSyntax { // Print the node's AST to understand the structure print("=== DEBUG MACRO ===") print("Node: \(node)") print("Arguments: \(node.argumentList)") // Complete dump of the syntax tree dump(node) // Continue with normal expansion return "42" } } ``` ### swift-ast-explorer ile AST'yi keşfetmek Çevrim içi araç [swift-ast-explorer.com](https://swift-ast-explorer.com), herhangi bir Swift kodunun sözdizimi ağacını görselleştirmeyi sağlar. Bir makro uygulanırken AST düğümleri arasında nasıl gezinileceğini anlamak için vazgeçilmezdir. ## Swift Macros için iyi uygulamalar Bakımı kolay makrolar oluşturmak belirli kurallara uymayı ve sık karşılaşılan tuzaklardan kaçınmayı gerektirir. ### Doğrulama ve hata mesajları ```swift // ValidationBestPractices.swift public struct ValidatedMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { // ✅ Validate usage context guard declaration.is(StructDeclSyntax.self) else { // ✅ Clear error messages with possible localization context.diagnose( Diagnostic( node: node, message: MacroDiagnosticMessage( id: "invalid-target", message: "This macro can only be applied to structs", severity: .error ) ) ) return [] } // ✅ Check required arguments guard let arguments = node.arguments else { context.diagnose( Diagnostic( node: node, message: MacroDiagnosticMessage( id: "missing-args", message: "Required arguments missing", severity: .error ) ) ) return [] } // Implementation... return [] } } // Structure for diagnostic messages struct MacroDiagnosticMessage: DiagnosticMessage { let id: String let message: String let severity: DiagnosticSeverity var diagnosticID: MessageID { MessageID(domain: "MyMacros", id: id) } } ``` ### Okunabilir kod üretmek ```swift // ReadableCodeGeneration.swift // ❌ Hard-to-read generated code let badCode: DeclSyntax = "public init(a:Int,b:String,c:Bool){self.a=a;self.b=b;self.c=c}" // ✅ Properly formatted generated code let goodCode: DeclSyntax = """ public init( a: Int, b: String, c: Bool ) { self.a = a self.b = b self.c = c } """ ``` Üretilen kod, geliştiriciler tarafından "Expand Macro" üzerinden incelendiği için elle yazılmış kod kadar okunaklı olmalıdır. ## Sonuç Swift Macros, statik tip güvenliğinden ödün vermeden boilerplate'i ortadan kaldırmak için güçlü bir araçtır. Bu teknoloji şunları mümkün kılar: **Önemli noktalar:** - ✅ İki kategori: freestanding (`#`) ve attached (`@`) - ✅ Beş attached rolü: peer, accessor, member, memberAttribute, conformance - ✅ swift-syntax ve AST manipülasyonuyla uygulama - ✅ `SwiftSyntaxMacrosTestSupport` ile zorunlu testler - ✅ Uygulamalar için ayrı paket gereksinimi - ✅ Xcode'daki "Expand Macro" üzerinden hata ayıklama - ✅ Geliştirici deneyimi için kritik olan açık hata mesajları Swift Macros özellikle uyumların (Equatable, Codable) üretilmesinde, ileri seviye property wrapper oluşturmada ve callback tabanlı API'leri async/await yönüne modernize etmede oldukça yararlıdır. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/tr/blog/ios/swift-macros-practical-metaprogramming-examples