Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
134: Introduction to Swift Macros
Wait, are macros just fancy functions that run at runtime?
This is the biggest misconception when people first see macros. No, they aren't. Unlike a function that executes while your app is running on a user's device, a Swift macro is a compile-time transformation. Think of it as a piece of code that writes other code for you before the compiler even finishes building your binary.
When you attach a macro to a variable or a function, the Swift compiler hands the source code over to the macro implementation, which then spits back a modified version of that code. By the time your app actually runs, the macro is gone—only the expanded code remains. This is why macros are so powerful for performance; there's zero runtime overhead for the "magic."
What's the actual difference between an "Attached" and a "Freestanding" macro?
You'll see these two terms everywhere in the documentation. The simplest way to remember it is by looking at the syntax: one uses an @ and the other uses a #.
- Attached Macros: These are "stuck" to a declaration. They can add new properties, add conformances to protocols, or even add entirely new functions to a class or struct. For example, imagine a
@Loggedmacro that automatically adds alogEntry()method to any class it's attached to. - Freestanding Macros: These stand on their own, usually inside a function body. They look like
#expression. A great example is the built-in#stringify, which takes a value and turns it into a literal string of the code used to create that value.
I usually reach for Attached macros when I'm trying to kill boilerplate in my models, and Freestanding macros when I need to capture metadata about the code itself.
Do I really have to learn a whole new library just to write one?
Yeah, you do—and it's the part that usually scares people off. To write the implementation of a macro, you have to use SwiftSyntax. Instead of working with variables and loops, you're working with "Syntax Trees."
Essentially, the compiler views your code as a tree of nodes. A FunctionDeclSyntax node represents a function declaration; a VariableDeclSyntax represents a variable. To make a macro, you write a piece of code that traverses this tree and modifies the nodes. It feels a bit like doing surgery on your own source code.
// This is what the "expanded" code looks like to the compiler
// Original:
@Logged
func saveUser() { ... }
// Expanded (what the macro generated):
func saveUser() { ... }
extension User {
func logSaveUser() { print("Entering saveUser...") }
}
It's a steep learning curve, but once you get the hang of the tree structure, you realize you have total control over how your code is generated.
When should I actually use a macro instead of just a protocol or a generic?
Be careful here. It's easy to get "macro fever" and try to automate everything. I always tell my juniors: if you can solve it with a protocol extension or a generic function, do that first. Macros are harder to debug and increase compile times.
Use a macro when you find yourself writing the exact same "boilerplate" patterns in dozens of different places—patterns that a protocol can't handle because they require adding actual members (like properties or methods) to a type dynamically. If you're spending your Friday afternoon copy-pasting the same five lines of setup code into twenty different structs, that's when you build a macro.
📋 Practical Task
Exercise: Designing a @Trace Method Macro
You are tasked with designing the logic for an Attached Macro called @Trace. This macro should be applied to functions. Instead of writing the full SwiftSyntax implementation (which requires a separate package), you will write the expansion logic.
Assume you have a function:
@Trace
func calculateTotal(price: Double, tax: Double) -> Double {
return price + tax
}
In a text editor or a comment block, write out exactly what the expanded code should look like after the macro runs. Your expansion must:
- Keep the original function exactly as it is.
- Add a new function immediately following it called
trace_calculateTotal(). - The new function should print the string:
"Tracing: calculateTotal".
Once you've written the expansion, explain why this must be an Attached macro rather than a Freestanding macro.
There are no comments for now.