Lesson 98Advanced Topics

Swift Macros

Compile-Time Code Generation

Swift Macros (introduced in Swift 5.9) transform your code at compile time. They eliminate boilerplate while maintaining type safety.

Macro Types

# Freestanding

Standalone, produces value

#stringify, #warning

@ Attached

Attached to declarations

@Observable, @Model

Built-in Macros

  • #available - Check OS version
  • #file, #line, #function - Debug info
  • #warning, #error - Compile messages
  • @Observable - Automatic observation
  • @Model - SwiftData persistence

Attached Macro Types

  • @attached(member) - Add properties/methods
  • @attached(accessor) - Add getters/setters
  • @attached(peer) - Add alongside declaration
  • @attached(conformance) - Add protocols

Debugging Macros

In Xcode, right-click any macro usage and select "Expand Macro" to see the generated code. This helps understand what the macro produces.

main.swift
// SWIFT MACROS (Swift 5.9+)
// Generate code at compile time

// ===== WHAT ARE MACROS? =====
// Macros transform your source code at compile time
// They reduce boilerplate and add functionality

// ===== FREESTANDING MACROS (#) =====
// Produce a value or perform an action

// #stringify - Returns expression as string with value
let (value, code) = #stringify(2 + 3)
// value = 5, code = "2 + 3"

// #warning and #error - Compile-time messages
#warning("This needs to be fixed before release")
// #error("This code should never be used")

// #file, #line, #function - Debug info
func logLocation() {
    print("Called from \(#file):\(#line) in \(#function)")
}

// ===== ATTACHED MACROS (@) =====
// Attached to declarations

// @Observable (Swift 5.9) - Automatic observation
@Observable
class Counter {
    var count = 0
    var name = "Counter"
}
// Expands to add observation tracking

// @Model (SwiftData) - Database model
// @Model
// class User {
//     var name: String
//     var age: Int
// }

// ===== COMMON BUILT-IN MACROS =====

// #available - Check API availability
if #available(iOS 17, *) {
    print("iOS 17+ features available")
}

// #selector - Create Objective-C selectors
// let sel = #selector(viewDidLoad)

// #keyPath - Type-safe key paths
class Person {
    @objc dynamic var name = ""
}
// let path = #keyPath(Person.name)

// #colorLiteral, #imageLiteral - Xcode literals
// let color = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1)

// ===== CREATING CUSTOM MACROS =====
// Macros are defined in separate Swift packages

// 1. Freestanding Expression Macro
// @freestanding(expression)
// macro stringify<T>(_ value: T) -> (T, String)

// 2. Attached Member Macro
// @attached(member)
// macro Observable()

// 3. Attached Peer Macro
// @attached(peer)
// macro AddAsync()

// ===== MACRO IMPLEMENTATION =====
// Macros use SwiftSyntax to transform AST

// import SwiftSyntax
// import SwiftSyntaxMacros
//
// struct StringifyMacro: ExpressionMacro {
//     static func expansion(
//         of node: some FreestandingMacroExpansionSyntax,
//         in context: some MacroExpansionContext
//     ) -> ExprSyntax {
//         // Transform syntax here
//     }
// }

// ===== MACRO TYPES =====
// @freestanding(expression) - Produces a value
// @freestanding(declaration) - Creates declarations
// @attached(peer) - Adds alongside declaration
// @attached(accessor) - Adds get/set
// @attached(member) - Adds members to type
// @attached(memberAttribute) - Adds attributes to members
// @attached(conformance) - Adds protocol conformance

// ===== PRACTICAL EXAMPLES =====

// Auto-generate CodingKeys
// @CodingKeys
// struct User {
//     var userName: String  // -> "user_name"
//     var emailAddress: String  // -> "email_address"
// }

// Auto-implement Equatable
// @AutoEquatable
// struct Point {
//     var x: Int
//     var y: Int
// }

// Generate mock for testing
// @Mock
// protocol NetworkService {
//     func fetch() async throws -> Data
// }

// ===== BENEFITS OF MACROS =====
// - Reduce boilerplate code
// - Compile-time safety
// - Better than runtime reflection
// - Clear expansion (can inspect in Xcode)

// ===== DEBUGGING MACROS =====
// Right-click macro usage in Xcode
// Select "Expand Macro" to see generated code

Try It Yourself!

Explore @Observable in a SwiftUI project and use "Expand Macro" to see the generated observation code!