# Swift Macros: praktische Beispiele für Metaprogrammierung > Vollständiger Leitfaden zu Swift Macros: Erstellung freistehender und attached Makros, AST-Manipulation mit swift-syntax und praktische Beispiele zur Vermeidung von Boilerplate-Code. - Published: 2026-03-11 - Updated: 2026-05-01 - Author: SharpSkill - Tags: swift, ios, macros, metaprogramming, swift-syntax - Reading time: 16 min --- Swift Macros, die mit Swift 5.9 und Xcode 15 eingeführt wurden, stellen eine Revolution beim Schreiben von Swift-Code dar. Das Feature ermöglicht Code-Generierung zur Compile-Zeit und beseitigt Boilerplate, ohne die statische Typsicherheit aufzugeben. Anders als die Makros des C-Präprozessors sind Swift Macros typsicher, in den Compiler integriert und werden vollständig von den Entwicklungstools unterstützt. > **Was dieser Leitfaden abdeckt** > > Dieser Leitfaden behandelt die Erstellung von Swift Macros von Anfang bis Ende: von den grundlegenden Konzepten bis hin zu fortgeschrittenen Implementierungen, mit funktionierenden Codebeispielen, die sich in jedem iOS-Projekt anpassen lassen. ## Die Typen von Swift Macros verstehen Swift bietet zwei Hauptkategorien von Makros mit jeweils eigenen Anwendungsfällen. Freistehende Makros wirken eigenständig als Ausdrücke oder Deklarationen, während attached Makros sich an bestehende Deklarationen binden, um sie zu modifizieren oder zu erweitern. ### Freistehende Makros: Expression und Declaration Freistehende Makros beginnen mit dem Symbol `#` und können entweder einen Wert zurückgeben (Expression) oder neue Deklarationen erzeugen. Es folgt ein konkretes Beispiel für ein Expression-Makro: ```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 ``` Der wesentliche Unterschied zwischen Expression und Declaration liegt im Ergebnis: Eine Expression liefert einen Wert, eine Declaration erzeugt strukturellen Code (Typen, Funktionen, Variablen). ### Attached Makros: die fünf Rollen Attached Makros beginnen mit `@` und stehen vor einer Deklaration. Swift definiert fünf unterschiedliche Rollen für diese Makros: ```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 { ... } ``` Diese Rollen lassen sich kombinieren, um leistungsfähige Makros zu erstellen, die Code in mehreren Dimensionen gleichzeitig transformieren. ## Projekteinrichtung für die Erstellung von Makros Für das Erstellen von Swift Macros wird ein Swift Package mit einer bestimmten Struktur benötigt. Das Package hängt von swift-syntax ab, der offiziellen Bibliothek zur Bearbeitung von Swift-Code als abstrakter Syntaxbaum (AST). ### Aufbau der Package.swift ```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") ] ) ] ) ``` Diese Konfiguration trennt die Makro-Deklarationen (was der Client-Code sieht) klar von ihrer Implementierung (die zur Compile-Zeit ausgeführt wird). > **Empfohlene Organisation** > > Mindestens drei Dateien werden benötigt: MyMacros.swift für die Deklarationen, MyMacrosPlugin.swift für die Implementierungen und MyMacrosTests.swift für die Tests. Diese Trennung erleichtert die Wartung. ## Ein Expression-Makro erstellen Expression-Makros erzeugen einen Wert, der direkt im Code verwendbar ist. Hier wird ein `#unwrap`-Makro erstellt, das ein Optional auspackt und im Fehlerfall den Variablennamen mitliefert. ### Deklaration des Makros ```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" ) ``` Die Signatur deklariert, dass das Makro ein Optional entgegennimmt und den nicht-optionalen Wert zurückgibt. `#externalMacro` verweist auf die Implementierung im Plugin. ### Implementierung mit swift-syntax ```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)" } } } ``` Die Methode `expansion` erhält den AST-Knoten, der den Makroaufruf darstellt, sowie den Compile-Kontext. Sie liefert ein `ExprSyntax`, das den generierten Code enthält. ### Plugin-Registrierung ```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 ] } ``` Dieser Einstiegspunkt informiert den Compiler über die im Plugin verfügbaren Makros. ## Ein attached Member-Makro erstellen Member-Makros fügen einem bestehenden Typ Mitglieder hinzu (Eigenschaften, Methoden, verschachtelte Typen). Hier folgt ein `@AutoInit`-Makro, das automatisch einen Memberwise-Initializer generiert. ### Vollständige Deklaration und Implementierung ```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 } } ``` ### Verwendung des AutoInit-Makros ```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 ``` Dieses Makro beseitigt das Boilerplate des Initializers und ist besonders bei Datenmodellen mit vielen Eigenschaften nützlich. ## Attached Peer-Makro für asynchrone Generierung Peer-Makros fügen Deklarationen auf derselben Ebene wie die annotierte Deklaration hinzu. Hier folgt ein `@AddAsync`-Makro, das eine async-Version einer auf Completion-Handler basierenden Funktion generiert. ```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" } } ``` ### Demonstration des AddAsync-Makros ```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) ``` > **Einschränkungen von Peer-Makros** > > Der Name der generierten Funktion muss in `names:` des `@attached`-Attributs deklariert werden. Hier zeigt `suffixed(Async)`, dass die generierte Funktion das Suffix "Async" erhält, das an den ursprünglichen Namen angehängt wird. ## Unit-Tests für Makros Das Testen von Makros ist unverzichtbar, da sie Code erzeugen, der anschließend kompiliert wird. Swift stellt `SwiftSyntaxMacrosTestSupport` bereit, um diese Tests zu erleichtern. ```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 ) } } ``` Die Tests prüfen die korrekte Code-Expansion sowie passende Fehlermeldungen bei unzulässiger Verwendung. ## Fortgeschrittenes Makro: beobachtbarer Property Wrapper Dieses Makro kombiniert mehrere Rollen, um ein Beobachtungssystem für Eigenschaften mit automatischen Benachrichtigungen aufzubauen. ```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] } } ``` ### Verwendung des Observable-Patterns ```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 setzt dieses Muster im neuen Observation-Framework seit Swift 5.9 ein. ## Makros debuggen und untersuchen Xcode bietet mehrere Werkzeuge, um Makros zu debuggen und den generierten Code nachzuvollziehen. ### Expansion in Xcode ```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 ``` ### Logging während der Entwicklung ```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" } } ``` ### AST mit swift-ast-explorer erkunden Das Online-Tool [swift-ast-explorer.com](https://swift-ast-explorer.com) zeigt den Syntaxbaum eines beliebigen Swift-Codes an. Es ist unverzichtbar, um beim Implementieren eines Makros nachzuvollziehen, wie sich AST-Knoten durchqueren lassen. ## Best Practices für Swift Macros Wartbare Makros entstehen nur, wenn bestimmte Konventionen eingehalten und typische Stolperfallen vermieden werden. ### Validierung und Fehlermeldungen ```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) } } ``` ### Lesbaren Code erzeugen ```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 } """ ``` Der generierte Code sollte genauso lesbar sein wie handgeschriebener Code, da er von Entwicklerinnen und Entwicklern über "Expand Macro" inspiziert wird. ## Fazit Swift Macros sind ein leistungsstarkes Werkzeug, um Boilerplate zu beseitigen, ohne die statische Typsicherheit aufzugeben. Diese Technologie ermöglicht: **Wichtigste Punkte:** - ✅ Zwei Kategorien: freistehend (`#`) und attached (`@`) - ✅ Fünf attached-Rollen: peer, accessor, member, memberAttribute, conformance - ✅ Implementierung über swift-syntax und AST-Manipulation - ✅ Pflichttests mit `SwiftSyntaxMacrosTestSupport` - ✅ Eigenes Package für die Implementierungen erforderlich - ✅ Debugging über "Expand Macro" in Xcode - ✅ Aussagekräftige Fehlermeldungen sind entscheidend für die Developer Experience Swift Macros sind besonders nützlich, um Konformanzen (Equatable, Codable) zu erzeugen, fortgeschrittene Property Wrapper zu bauen und auf Callbacks basierende APIs in Richtung async/await zu modernisieren. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/ios/swift-macros-practical-metaprogramming-examples