Eliminating Boilerplate with Swift Macros in Xcode 15
Stop writing repetitive glue code. Learn how Swift Macros in Xcode 15 move code generation to compile-time, providing type-safe expansions and eliminating boilerplate.
08 Apr 2026, 06:36 UTC

Writing repetitive glue code—like manually implementing CodingKeys, setting up observation patterns, or creating backing stores for configuration files—slows down development and introduces subtle bugs. For years, Swift developers relied on runtime reflection or external code generation scripts to handle this, but these methods often lacked type safety or complicated the build pipeline.
Swift Macros, introduced in Swift 5.9 and integrated into Xcode 15, solve this by moving code generation into the compiler. Instead of using reflection at runtime, macros act as compile-time functions that take your source code as input and emit new, type-safe Swift code. The result is a declarative syntax that removes boilerplate while remaining fully visible to the compiler's type checker.
How Macros Transform Source Code
A macro is not a function that runs when your app is running; it is a plugin that runs during the build process. It uses swift-syntax, a library that parses Swift code into a Syntax Tree (a structured representation of your code). The macro examines this tree and returns a new piece of syntax to be inserted into your program.
There are several types of macros, but the most common are Attached Macros. These can add members (like new variables or functions), add conformance to a protocol, or add attributes to a declaration. For example, the @Observable macro in SwiftUI automatically synthesizes the necessary registration code to notify views when a property changes, replacing the manual ObservableObject and @Published pattern.
Implementing a Custom Storage Macro
Consider a scenario where you need multiple properties backed by a dictionary (such as a UserDefaults wrapper). Manually writing getters and setters for every property is tedious. A @DictionaryStorage macro can automate this by generating a private dictionary and the corresponding accessors.
The Implementation Strategy
To build this, you create a Macro package using File > New > Package > Macro. This creates two main targets: the macro definition (the interface) and the macro implementation (the logic).
// The Interface (What the user sees)
@attached(member, names: arbitrary)
public macro DictionaryStorage() = #externalMacro(module: "MyMacroImplementation", type: "DictionaryStorageMacro")
In the implementation module, you define a struct that conforms to MemberMacro. This struct analyzes the properties of the class or struct it is attached to and generates the following code behind the scenes:
// The generated expansion for a property 'var username: String'
private var _storage: [String: Any] = [:]
var username: String {
get { _storage["username"] as? String ?? "" }
set { _storage["username"] = newValue }
}
Verification and Testing
Because macros run at compile time, you cannot use standard breakpoints. Instead, Xcode provides a test target using assertMacroExpansion. You write a test case that provides a string of source code and asserts that the expanded output matches your expected result. This TDD (Test-Driven Development) approach is the only reliable way to ensure your macro handles edge cases like optional types or generics.
Debugging and Inspecting Expansions
One of the biggest hurdles with generated code is the "black box" effect. To see exactly what the macro is doing, use the Xcode editor feature: Editor > Show Expanded Source. This opens a window showing the final code after the macro has run, allowing you to verify that the generated properties and types are correct.
If a macro produces invalid Swift code, the error will appear at the call site in your main project. If the macro author provided a Diagnostic, Xcode will surface a specific fix-it suggestion in the Issue Navigator, making the error feel like a native compiler warning.
Constraints and Trade-offs
While powerful, macros operate within a strict sandbox. For security and build determinism, the macro plugin process cannot access the network or read arbitrary files from your hard drive. This means you cannot build a macro that reads a JSON schema file from your disk to generate properties; such data must be passed in as a literal argument to the macro or handled by a separate build-phase script.
| Feature | Traditional Code Gen (Scripts) | Swift Macros |
|---|---|---|
| Execution Time | Pre-build / Manual | During Compilation |
| Type Safety | Delayed until build | Immediate / Integrated |
| Visibility | Generated files in project | Hidden (unless expanded) |
| Environment Access | Full System Access | Sandboxed |
Practical Adoption
To start using macros, ensure you are on Xcode 15+ and Swift 5.9+. If you are developing a library for others, remember that macros increase the complexity of the dependency graph since the compiler must compile the macro implementation before it can compile the code that uses it. For internal app development, macros are best applied to repetitive patterns like API response mapping or persistence layers where the boilerplate exceeds 20% of the file length.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.