# Swift Macros: Practical Metaprogramming Examples > Complete guide to Swift Macros: creating freestanding and attached macros, AST manipulation with swift-syntax, and practical examples to eliminate boilerplate code. - Published: 2026-03-11 - Updated: 2026-03-28 - Author: SharpSkill - Tags: swift, ios, macros, metaprogramming, swift-syntax - Reading time: 16 min --- Swift Macros, introduced with Swift 5.9 and Xcode 15, represent a revolution in how Swift code is written. This feature enables compile-time code generation, eliminating boilerplate while preserving static type safety. Unlike C preprocessor macros, Swift macros are type-safe, compiler-integrated, and fully supported by development tools. > **What this guide covers** > > This guide explores Swift macro creation from start to finish: from fundamental concepts to advanced implementations, with working code examples ready to adapt in any iOS project. ## Understanding Swift Macro Types Swift provides two main categories of macros, each with distinct use cases. Freestanding macros work autonomously as expressions or declarations, while attached macros bind to existing declarations to modify or enhance them. ### Freestanding Macros: Expression and Declaration Freestanding macros begin with the `#` symbol and can either return a value (expression) or create new declarations. Here's a concrete expression macro example: ```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 ``` The fundamental difference between expression and declaration lies in the result: an expression produces a value, a declaration produces structural code (types, functions, variables). ### Attached Macros: The Five Roles Attached macros begin with `@` and are placed before a declaration. Swift defines five distinct roles for these macros: ```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 { ... } ``` These roles can be combined to create powerful macros that transform code in multiple ways simultaneously. ## Project Setup for Creating Macros Creating Swift macros requires a Swift Package with a specific structure. The package depends on swift-syntax, the official library for manipulating Swift code as an abstract syntax tree (AST). ### Package.swift Structure ```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") ] ) ] ) ``` This configuration clearly separates macro declarations (what client code sees) from their implementation (executed at compile time). > **Recommended organization** > > Three files minimum are needed: MyMacros.swift for declarations, MyMacrosPlugin.swift for implementations, and MyMacrosTests.swift for tests. This separation eases maintenance. ## Creating an Expression Macro Expression macros generate a value usable in code. Here's how to create an `#unwrap` macro that unwraps an optional with a custom error message including the variable name. ### Macro Declaration ```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" ) ``` The signature declares that the macro takes an optional and returns the non-optional value. `#externalMacro` points to the implementation in the plugin. ### Implementation with 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)" } } } ``` The `expansion` method receives the AST node representing the macro call and the compilation context. It returns an `ExprSyntax` containing the generated code. ### Plugin Registration ```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 ] } ``` This entry point informs the compiler about available macros in this plugin. ## Creating an Attached Member Macro Member macros add members (properties, methods, nested types) to an existing type. Here's an `@AutoInit` macro that automatically generates a memberwise initializer. ### Complete Declaration and Implementation ```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 } } ``` ### Using the AutoInit Macro ```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 ``` This macro eliminates initializer boilerplate, particularly useful for data models with many properties. ## Attached Peer Macro for Async Generation Peer macros add declarations at the same level as the annotated declaration. Here's an `@AddAsync` macro that generates an async version of a completion-based function. ```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 Macro Demonstration ```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 macro limitations** > > The generated function name must be declared in `names:` of the `@attached` attribute. Here, `suffixed(Async)` indicates the generated function will have the "Async" suffix added to the original name. ## Unit Testing Macros Testing macros is essential since they generate code that will be compiled. Swift provides `SwiftSyntaxMacrosTestSupport` to facilitate these tests. ```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 ) } } ``` Tests verify correct code expansion and appropriate error messages for invalid usage. ## Advanced Macro: Observable Property Wrapper This macro combines multiple roles to create a property observation system with automatic notifications. ```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] } } ``` ### Using the Observable Pattern ```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 // } // } // } } ``` This pattern is used by Apple in the new Observation framework from Swift 5.9+. ## Debugging and Inspecting Macros Xcode offers several tools for debugging macros and understanding generated code. ### 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 During Development ```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" } } ``` ### Exploring AST with swift-ast-explorer The online tool [swift-ast-explorer.com](https://swift-ast-explorer.com) allows visualization of the syntax tree for any Swift code. It's essential for understanding how to navigate AST nodes when implementing macros. ## Swift Macro Best Practices Creating maintainable macros requires following certain conventions and avoiding common pitfalls. ### Validation and Error Messages ```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) } } ``` ### Generating Readable Code ```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 } """ ``` Generated code should be as readable as manually written code, since developers will inspect it via "Expand Macro". ## Conclusion Swift Macros represent a powerful tool for eliminating boilerplate while preserving static type safety. This technology enables: **Key takeaways:** - ✅ Two categories: freestanding (`#`) and attached (`@`) - ✅ Five attached roles: peer, accessor, member, memberAttribute, conformance - ✅ Implementation via swift-syntax and AST manipulation - ✅ Mandatory testing with `SwiftSyntaxMacrosTestSupport` - ✅ Separate package required for implementations - ✅ Debugging via "Expand Macro" in Xcode - ✅ Explicit error messages essential for developer UX Swift macros are particularly useful for generating conformances (Equatable, Codable), creating advanced property wrappers, and modernizing callback-based APIs to async/await. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/ios/swift-macros-practical-metaprogramming-examples