Working with Swift Macros
1. Understanding Macro Types
| Type | Description | Usage Example | Notes |
|---|---|---|---|
| Expression Macro | Expands to a single expression | |
Used for inline computations |
| Declaration Macro | Expands to one or more declarations | |
Can generate functions, variables, etc. |
| Member Attribute Macro | Applies to members of a type | |
Used to add behavior to properties or methods |
| Peer Attribute Macro | Applies to a type or its members | |
Can modify type definitions or their members |
2. Using Built-in Macros (#warning, #error)
| Macro | Description | Usage | Notes |
|---|---|---|---|
| #warning | Triggers a compilation warning with a custom message. | #warning("Review this implementation") |
Does not stop compilation, only shows a warning. |
| #error | Triggers a compilation error with a custom message. | #error("This feature is not implemented yet") |
Stops compilation with an error message. |
Example: Using #warning and #error
struct MyStruct {
#warning("Implement proper validation logic here")
var value: Int = 0
#error("This class is deprecated, use MyNewClass instead")
}
3. Using @Observable Macro
The @Observable macro in Swift is a powerful tool that automatically generates observable properties and change tracking for your models. It simplifies the process of making your data structures compatible with SwiftUI's view updates.
| Property | Description | Usage |
|---|---|---|
| @Observable | Applies to a class or struct to make its properties observable. | @Observable must be placed before the class or struct declaration. |
Example: Basic Usage of @Observable
@Observable
class User {
var name: String = "John"
var age: Int = 30
}
Example: Using @Observable with Property Observers
@Observable
class UserProfile {
var name: String = "Jane"
var email: String = "jane@example.com"
var displayName: String {
didSet {
print("Display name changed to \(<span class="property">displayName</span>)")
}
}
}
@Observable macro automatically generates the necessary change tracking and observable property wrappers. You don't need to manually implement @Published or similar.@Observable macro is available starting in Swift 5.9 and requires Xcode 15.0 or later.4. Using @Model Macro for SwiftData
| Property | Value | Description |
|---|---|---|
| @Model | struct User { ... } |
Declares a model type for SwiftData |
| @Attribute | @Attribute var name: String |
Defines a property as an attribute in SwiftData |
| @Relationship | @Relationship var posts: [Post] |
Defines a relationship to another model |
Example: Define a SwiftData Model
@Model
final class User {
var name: String
var email: String
init(name: String, email: String) {
self.name = name
self.email = email
}
}
Example: Using Relationships
@Model
final class Post {
var title: String
@Relationship var author: User
init(title: String, author: User) {
self.title = title
self.author = author
}
}
@Model macro automatically synthesizes the required SwiftData storage methods and properties.@Attribute are of supported types (String, Int, Double, Bool, Date, etc.)5. Creating Freestanding Expression Macros
Freestanding expression macros in Swift allow you to define macros that can be used as expressions, not just statements. These macros are expanded at compile time and can transform Swift code.
| Property | Description | Example |
|---|---|---|
| macro | Defines a freestanding expression macro | macro myMacro() = #externalMacro(module: "MyMacros", type: "MyMacro") |
| macro signature | Defines the macro's input and output types | macro myMacro<T>(value: T) -> T |
Example: Basic Freestanding Expression Macro
import SwiftSyntax
import SwiftSyntaxBuilder
macro myMacro(value: Int) -> Int = #externalMacro(module: "MyMacros", type: "MyMacro")
// Usage in code:
let result = myMacro(value: 5)
6. Creating Attached Member Macros
Attached member macros in Swift allow you to inject new members into types that conform to a protocol or extend a type. These macros are defined using the @attached(member) attribute.
| Attribute | Description | Usage |
|---|---|---|
| @attached(member) | Defines a macro that injects members into a type | @attached(member) macro myMacro() |
Example: Basic Attached Member Macro
@attached(member) macro logPropertyChanges() = #externalMacro(module: "MyMacros", type: "LogPropertyChangesMacro")
struct Person {
@logPropertyChanges
var name: String = "John"
}
MyMacros.
Key Points
- Use
@attached(member)to define macros that inject members into types - Macro implementations must be in a separate module
- Can be used to add computed properties, methods, or other members
7. Understanding Macro Expansion
Macro expansion in Swift is the process by which macro definitions are transformed into actual code at compile time. This transformation happens before the compiler processes the code, allowing macros to generate or modify Swift source code.
| Phase | Description | Result |
|---|---|---|
| Declaration | Macro is declared using @macro or macro keyword |
Macro definition is registered in the compiler |
| Expansion | Macro is invoked in code, compiler processes macro body | Generated Swift code replaces the macro invocation |
| Compilation | Generated code is compiled as normal Swift code | Final executable or library is created |
Example: Basic Macro Expansion
macro log(_ message: String) = #externalMacro(module: "LogMacros", type: "LogMacro")
// Usage in code:
log("Hello, World!")
// Expands to:
print("Hello, World!")
Expansion Process Overview
- Declaration: Macro is defined with a signature and implementation
- Invocation: Macro is called in Swift code using its name and arguments
- Compilation: Generated code is compiled as part of the normal Swift compilation process
8. Testing Macros
Example: Basic Macro Test
import XCTest
import SwiftSyntax
import SwiftSyntaxBuilder
final class MacroTests: XCTestCase {
func testMacroExpansion() {
let source = "let x = 42"
let expanded = try! expandMacro(source, macroName: "myMacro")
XCTAssertEqual(expanded, "let x = 42")
}
}
| Test Type | Description | Notes |
|---|---|---|
| Unit Test | Test macro behavior in isolation | Use XCTest framework |
| Integration Test | Test macro in context of full compilation | Requires swift build or Xcode build |
Testing Strategy
- Use
XCTestfor unit-level macro validation - Validate syntax tree transformations
- Ensure macro behavior matches expected output
9. Debugging Macro Output
Example: Using dump in Macro Definitions
import SwiftSyntax
import SwiftSyntaxBuilder
struct MyMacro: ExpressionMacro {
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExpressionSyntax {
let dumpedValue = "Hello, World!"
print(dumpedValue)
return "\(dumpedValue)"
}
}
| Approach | Description | Notes |
|---|---|---|
| Print Debugging | print() or dump() |
Useful for inspecting macro-generated code at compile time |
| Xcode Console | Compile-time logs in Xcode console | Output appears during compilation, not runtime |
10. Following Macro Best Practices
| Best Practice | Description | Implementation |
|---|---|---|
| Use descriptive names | Macro names should clearly indicate their purpose. | @available(iOS 17.0, *) macro MyMacro() = #externalMacro(module: "MyMacros", type: "MyMacro") |
| Keep macros simple | Avoid complex logic inside macros; keep them focused on code generation. | macro simpleMacro() = #externalMacro(module: "MyMacros", type: "SimpleMacro") |
| Document macro usage | Provide clear documentation for how to use the macro. | Example: Documentation |
| Test macro behavior | Ensure macros behave as expected with various inputs. | |
| Avoid runtime dependencies | Macros should not rely on runtime libraries or external dependencies. | macro noRuntimeDep() = #externalMacro(module: "MyMacros", type: "NoRuntimeDep") |
Summary of Macro Best Practices
- Use descriptive names for clarity and maintainability.
- Keep macro logic simple to prevent complexity and errors.
- Document usage to help other developers understand the macro's purpose.
- Test macros thoroughly to ensure correctness across different inputs.
- Avoid runtime dependencies to ensure macro functionality at compile time.