Skip to content
All essays
iOSMarch 27, 202513 min

Swift Macros: Code Generation and Metaprogramming

Master Swift macros for code generation. Freestanding and attached macros for metaprogramming.

Ü
Ümit Uz
Mobile & Full Stack Developer

Swift macros enable powerful code generation while maintaining type safety. Learn to write your own macros.

Macro Basics

What Are Macros?

swift
// Before macros - repetitive code
struct User {
  let id: String
  let name: String
  let email: String

  init(id: String, name: String, email: String) {
    self.id = id
    self.name = name
    self.email = email
  }

  var debugDescription: String {
    "User(id: \(id), name: \(name), email: \(email))"
  }
}

// With macros - cleaner code
@Codable
@MemberwiseInit
@DebugDescription
struct User {
  let id: String
  let name: String
  let email: String
}

Freestanding Macros

Build Expressions

swift
// Define macro
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(
  module: "MyMacros",
  type: "StringifyMacro"
)

// Implement macro
public struct StringifyMacro: ExpressionMacro {
  public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) throws -> ExprSyntax {
    guard let argument = node.argumentList.first?.expression else {
      throw MacroError.argumentRequired
    }

    return "((argument), \(literal: argument.description))"
  }
}

// Usage
let (value, string) = #stringify(x + y)
// value is the result of x + y
// string is "x + y"

Declaration Macros

swift
@freestanding(declaration, names: arbitrary)
public macro DictionaryStorage() = #externalMacro(
  module: "MyMacros",
  type: "DictionaryStorageMacro"
)

// Implementation
public struct DictionaryStorageMacro: DeclarationMacro {
  public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax] {
    return [
      """
      private var _storage: [String: Any] = [:]
      """
    ]
  }
}

// Usage
#DictionaryStorage

var name: String {
  get { _storage["name"] as? String ?? "" }
  set { _storage["name"] = newValue }
}

Attached Macros

Memberwise Init Macro

swift
@attached(member, names: arbitrary)
public macro MemberwiseInit() = #externalMacro(
  module: "MyMacros",
  type: "MemberwiseInitMacro"
)

public struct MemberwiseInitMacro: MemberMacro {
  public static func expansion<
    DeclarationContext: DeclGroupSyntax,
    MemberContext: DeclSyntaxProtocol
  >(
    of node: AttributeSyntax,
    providingMembersOf declaration: DeclarationContext,
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax] {
    let storedProperties = declaration.memberBlock.members
      .compactMap { $0.decl.as(VariableDeclSyntax.self) }
      .filter { $0.modifiers == nil || !$0.modifiers!.contains(keyword: "static") }

    let parameters = storedProperties.map { property in
      guard let binding = property.bindings.first,
            let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text,
            let type = binding.typeAnnotation?.type else {
        return ""
      }
      return "\(name): \(type)"
    }

    let assignments = storedProperties.map { property in
      guard let name = property.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
        return ""
      }
      return "self.\(name) = \(name)"
    }

    return [
      """
      init(\(parameters.joined(separator: ", "))) {
        \(assignments.joined(separator: "\n"))
      }
      """
    ]
  }
}

// Usage
@MemberwiseInit
struct User {
  let id: String
  let name: String
  let email: String
}

// Generates:
// init(id: String, name: String, email: String) {
//   self.id = id
//   self.name = name
//   self.email = email
// }

Codable Macro

swift
@attached(extension, conformances: Codable)
public macro CustomCodable() = #externalMacro(
  module: "MyMacros",
  type: "CustomCodableMacro"
)

// Usage
@CustomCodable
struct User {
  let id: String
  let name: String
  let email: String
}

// Generates Codable conformance automatically

Macro Types

Accessor Macro

swift
@attached(accessor)
public macro Lazy() = #externalMacro(
  module: "MyMacros",
  type: "LazyMacro"
)

// Usage
class DataProcessor {
  @Lazy var expensiveValue: Int = {
    // Expensive computation
    return 42
  }()
}

Attribute Macro

swift
@attached(peer)
public macro CLIOption() = #externalMacro(
  module: "MyMacros",
  type: "CLIOptionMacro"
)

// Usage
struct CLI {
  @CLIOption
  var verbose: Bool = false

  @CLIOption
  var output: String?
}

Building Macros

Package Structure

swift
// Package.swift
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
  name: "MyMacros",
  platforms: [.macOS(.v13)],
  dependencies: [
    .package(
      url: "https://github.com/apple/swift-syntax.git",
      from: "509.0.0"
    )
  ],
  targets: [
    .target(
      name: "MyMacros",
      dependencies: [
        .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
        .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
      ]
    ),
    .testTarget(
      name: "MyMacrosTests",
      dependencies: ["MyMacros"]
    )
  ]
)

Macro Implementation

swift
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import SwiftCompilerPlugin

public struct MyMacroPlugin: CompilerPlugin {
  public var providingMacros: [Macro.Type] = [
    StringifyMacro.self,
    MemberwiseInitMacro.self,
    DictionaryStorageMacro.self
  ]
}

public struct StringifyMacro: ExpressionMacro {
  public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) throws -> ExprSyntax {
    guard let argument = node.argumentList.first?.expression else {
      throw MacroError.argumentRequired
    }

    return "((argument), \(literal: argument.description))"
  }
}

enum MacroError: Error {
  case argumentRequired
}

Macro Tests

swift
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import MyMacros

final class MyMacrosTests: XCTestCase {
  func testStringifyMacro() {
    assertMacroExpansion(
      """
      #stringify(x + y)
      """,
      expandedSource: """
      (x + y, "x + y")
      """,
      macros: [
        "stringify": StringifyMacro.self
      ]
    )
  }

  func testMemberwiseInitMacro() {
    assertMacroExpansion(
      """
      @MemberwiseInit
      struct User {
        let id: String
        let name: String
      }
      """,
      expandedSource: """
      struct User {
        let id: String
        let name: String

        init(id: String, name: String) {
          self.id = id
          self.name = name
        }
      }
      """,
      macros: [
        "MemberwiseInit": MemberwiseInitMacro.self
      ]
    )
  }
}

Real-World Macros

Equatable Macro

swift
@attached(extension, conformances: Equatable)
public macro AutoEquatable() = #externalMacro(
  module: "MyMacros",
  type: "AutoEquatableMacro"
)

// Usage
@AutoEquatable
struct User {
  let id: String
  let name: String
  let email: String
}

// User automatically conforms to Equatable

Observable Macro

swift
@attached(member, names: arbitrary)
public macro Observable() = #externalMacro(
  module: "MyMacros",
  type: "ObservableMacro"
)

// Usage
@Observable
class ViewModel {
  var isLoading = false
  var items: [Item] = []
}

// Generates:
// class ViewModel: ObservableObject {
//   @Published var isLoading = false
//   @Published var items: [Item] = []
// }

Best Practices

  1. 1Keep Simple: Keep macros simple
  2. 2Debug Carefully: Debug macro expansions
  3. 3Test Thoroughly: Test macro outputs
  4. 4Document Well: Document macro behavior
  5. 5Type Safety: Maintain type safety
  6. 6Performance: Consider compile-time performance
  7. 7Error Messages: Provide clear errors
  8. 8Use Cases: Use appropriate use cases

Macros enable powerful metaprogramming in Swift!

Related essays

Next essay
Swift Concurrency Patterns: Advanced async/await