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
136: Writing an Attached Macro
I've lost count of how many times I've written a print("User: \(name), Email: \(email), ID: \(id)") statement just to see what's happening inside a struct during a debugging session. It's tedious, it's boilerplate, and it's exactly the kind of repetition that Swift macros were designed to kill. Today, we're going to build an attached macro called @Logged. When we slap this on a struct, it'll automatically generate a log() method that prints out all the stored properties of that struct.
Designing our @Logged macro
Since we want to add a new method to an existing type, we need a Member Macro. In the macro declaration, I'll specify that this is an attached(member) macro. I'm also going to give it a specific name—log—so the compiler knows exactly what this macro intends to produce. This helps with autocomplete and prevents the compiler from guessing.
@attached(member, names: named(log))
public macro Logged() = #externalMacro(module: "MyMacroImplementation", type: "LoggedMacro")
Digging into SwiftSyntax
Now we move over to the implementation side. This is where things get a bit dense because we're dealing with SwiftSyntax. We aren't writing Swift code; we're writing code that generates Swift code. I need to implement the MemberMacro protocol, specifically the expansion function. My goal here is to look at the members of the struct and build a string of print statements.
I'll start by filtering the members to find only the variables. I don't want to try and log methods or other nested types—that would be a mess.
public struct LoggedMacro: MemberMacro {
public static func expansion(
of node: some DeclGroupSyntax,
providingMembersOf declaration: some DeclSyntax
) throws -> [DeclSyntax] {
// Find all variable declarations in the struct
let variables = node.memberBlock.members.compactMap { member in
if let varDecl = member.as(VariableDeclSyntax.self) {
return varDecl
}
return nil
}
// We'll build our function body here
let properties = variables.map { varDecl in
// Extract the variable name
let name = varDecl.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text ?? "unknown"
return "\(name): \\(\(name))"
}.joined(separator: ", ")
return [
"func log() { print(\"Log: \(properties)\") }"
]
}
}
The 'Oops' moment: Handling types
I ran this against a simple User struct, and it worked great. But then I tried it on a struct that had an optional property, and I realized I'd made a classic mistake: I was assuming the properties I was capturing were simple identifiers. When I added a property with a default value or a complex type annotation, my compactMap logic started acting up, and the generated string was occasionally pulling in the type annotation instead of just the name.
The problem was that VariableDeclSyntax is a tree, not a string. I was grabbing the pattern, but I wasn't being strict enough about extracting just the identifier. If the variable had an attribute or a complex binding, my "unknown" fallback was triggering, or worse, it was producing invalid Swift code that wouldn't compile in the client target.
I fixed this by drilling deeper into the IdentifierPatternSyntax. I also realized that if the struct is empty, my log() function would print "Log: ", which is useless. I added a guard to ensure we actually have properties before generating the method.
// Fixed logic inside the expansion function
guard !variables.isEmpty else { return [] }
let propertyStrings = variables.compactMap { varDecl in
for binding in varDecl.bindings {
if let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text {
return "\(identifier): \\(\(identifier))"
}
}
return nil
}
let body = propertyStrings.joined(separator: ", ")
return ["func log() { print(\"Log: \(body)\") }"]
Connecting the macro to the client
Now that the implementation is robust, the usage is the best part. I just apply the attribute to any model I'm working with. Because it's an attached macro, the log() method doesn't exist in my source code, but it's available to the compiler and the IDE.
@Logged
struct Project {
let name: String
let budget: Int
let isInternal: Bool
}
let myProject = Project(name: "Mars Colony", budget: 1000000, isInternal: false)
myProject.log()
// Prints: Log: name: Mars Colony, budget: 1000000, isInternal: false
It's a small win, but it's a huge quality-of-life improvement. Instead of manually updating a print statement every time I add a field to my struct, the macro handles the bookkeeping for me.
📋 Practical Task
Implement the @AutoInit macro
Your task is to create an attached member macro called @AutoInit. Instead of a logging method, this macro should generate a convenience initializer for a struct that sets all its stored properties to "default" values.
- For
Stringproperties, the default should be"". - For
Intproperties, the default should be0. - For
Boolproperties, the default should befalse.
Requirements:
- Define the macro as an
attached(member)macro. - In the implementation, iterate through the
VariableDeclSyntaxmembers. - Inspect the type of the variable (you'll need to look at the
typeAnnotation) to determine which default value to use. - Generate a
init()method that assigns these values toself. - Ensure that if a property's type is not String, Int, or Bool, it is simply skipped in the initializer.
There are no comments for now.