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 automaticallyMacro 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 EquatableObservable 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
- 1Keep Simple: Keep macros simple
- 2Debug Carefully: Debug macro expansions
- 3Test Thoroughly: Test macro outputs
- 4Document Well: Document macro behavior
- 5Type Safety: Maintain type safety
- 6Performance: Consider compile-time performance
- 7Error Messages: Provide clear errors
- 8Use Cases: Use appropriate use cases
Macros enable powerful metaprogramming in Swift!