Swift Macros: ตัวอย่างเชิงปฏิบัติของ metaprogramming

คู่มือฉบับสมบูรณ์ของ Swift Macros: การสร้างมาโคร freestanding และ attached, การจัดการ AST ด้วย swift-syntax และตัวอย่างเชิงปฏิบัติเพื่อกำจัดโค้ดซ้ำซาก

Metaprogramming ด้วย Swift Macros และตัวอย่างโค้ดเชิงปฏิบัติ

Swift Macros ที่เปิดตัวพร้อมกับ Swift 5.9 และ Xcode 15 ถือเป็นการปฏิวัติวิธีเขียนโค้ด Swift โดยอนุญาตให้สร้างโค้ดในขั้นตอนคอมไพล์ ทำให้ลด boilerplate ลงโดยไม่สูญเสียความปลอดภัยของชนิดข้อมูลแบบสแตติก ต่างจากมาโครของพรีโพรเซสเซอร์ใน C ตรงที่ Swift Macros ปลอดภัยทางชนิด ผูกกับคอมไพเลอร์ และได้รับการสนับสนุนเต็มรูปแบบจากเครื่องมือพัฒนา

ขอบเขตของคู่มือฉบับนี้

คู่มือฉบับนี้พาสำรวจการสร้าง Swift Macros ตั้งแต่ต้นจนจบ ตั้งแต่แนวคิดพื้นฐานไปจนถึงการนำไปใช้งานขั้นสูง พร้อมตัวอย่างโค้ดที่ใช้งานได้จริงและพร้อมปรับใช้กับโปรเจกต์ iOS ใด ๆ

ทำความเข้าใจประเภทของ Swift Macros

Swift มีมาโครหลักสองประเภท แต่ละประเภทรองรับสถานการณ์การใช้งานที่ต่างกัน มาโครแบบ freestanding ทำงานเป็นนิพจน์หรือดีคลาเรชันโดยอิสระ ในขณะที่มาโคร attached จะแนบกับดีคลาเรชันเดิมเพื่อปรับเปลี่ยนหรือเสริมความสามารถ

มาโคร freestanding: นิพจน์และดีคลาเรชัน

มาโคร freestanding เริ่มต้นด้วยสัญลักษณ์ # และสามารถส่งคืนค่า (นิพจน์) หรือสร้างดีคลาเรชันใหม่ได้ ต่อไปนี้คือตัวอย่างเชิงรูปธรรมของมาโครนิพจน์:

MacroUsage.swiftswift
// Freestanding expression macro - generates a value
let buildInfo = #buildDate
// Expansion → "2026-03-11 10:30:45"

// Freestanding macro with arguments
let message = #stringify(1 + 2)
// Expansion → "1 + 2 = 3"

// Freestanding declaration macro - creates declarations
#makeCase("success", "failure", "pending")
// Expansion →
// case success
// case failure
// case pending

ความแตกต่างหลักระหว่างนิพจน์กับดีคลาเรชันอยู่ที่ผลลัพธ์: นิพจน์ให้ค่ากลับมา ส่วนดีคลาเรชันสร้างโค้ดเชิงโครงสร้าง (ชนิดข้อมูล ฟังก์ชัน ตัวแปร)

มาโคร attached: ห้าบทบาท

มาโคร attached เริ่มต้นด้วย @ และวางอยู่หน้าดีคลาเรชัน Swift กำหนดบทบาทห้าแบบสำหรับมาโครเหล่านี้:

AttachedMacroRoles.swiftswift
// @attached(peer) - adds declarations at the same level
@AddAsync
func fetchUser(id: Int) -> User { ... }
// Expansion → adds func fetchUserAsync(id: Int) async -> User

// @attached(accessor) - adds getters/setters
@UserDefault("theme")
var currentTheme: String
// Expansion → adds get { UserDefaults.standard.string(...) }

// @attached(member) - adds members to a type
@AutoEquatable
struct Point {
    var x: Int
    var y: Int
}
// Expansion → adds static func == (lhs: Point, rhs: Point) -> Bool

// @attached(memberAttribute) - applies attributes to members
@CodableKeys
struct Config {
    var apiUrl: String
    var timeout: Int
}
// Expansion → adds @CodingKey("api_url") before apiUrl

// @attached(conformance) / @attached(extension) - adds conformances
@Hashable
struct User {
    var id: Int
    var name: String
}
// Expansion → adds extension User: Hashable { ... }

บทบาทเหล่านี้สามารถผสานรวมกันเพื่อสร้างมาโครที่ทรงพลังและสามารถแปลงโค้ดได้หลายมิติพร้อมกัน

การตั้งค่าโปรเจกต์เพื่อสร้างมาโคร

การสร้าง Swift Macros ต้องใช้ Swift Package ที่มีโครงสร้างเฉพาะ แพ็กเกจนี้ขึ้นอยู่กับ swift-syntax ซึ่งเป็นไลบรารีทางการสำหรับจัดการโค้ด Swift ในรูปแบบของ abstract syntax tree (AST)

โครงสร้างของ Package.swift

Package.swiftswift
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyMacros",
    platforms: [.macOS(.v10_15), .iOS(.v13)],
    products: [
        // Library exposing macros to the main project
        .library(
            name: "MyMacros",
            targets: ["MyMacros"]
        ),
        // Executable for testing macros
        .executable(
            name: "MyMacrosClient",
            targets: ["MyMacrosClient"]
        )
    ],
    dependencies: [
        // Required dependency for macros
        .package(
            url: "https://github.com/apple/swift-syntax.git",
            from: "509.0.0"
        )
    ],
    targets: [
        // Compiler plugin containing implementation
        .macro(
            name: "MyMacrosPlugin",
            dependencies: [
                .product(name: "SwiftSyntax", package: "swift-syntax"),
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
            ]
        ),
        // Target exposing macro declarations
        .target(
            name: "MyMacros",
            dependencies: ["MyMacrosPlugin"]
        ),
        // Test client
        .executableTarget(
            name: "MyMacrosClient",
            dependencies: ["MyMacros"]
        ),
        // Unit tests
        .testTarget(
            name: "MyMacrosTests",
            dependencies: [
                "MyMacrosPlugin",
                .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
            ]
        )
    ]
)

การตั้งค่านี้แยกอย่างชัดเจนระหว่างการประกาศมาโคร (สิ่งที่โค้ดฝั่งไคลเอนต์เห็น) กับการนำไปใช้ (ที่ทำงานช่วงคอมไพล์)

การจัดระเบียบที่แนะนำ

ต้องมีไฟล์อย่างน้อยสามไฟล์: MyMacros.swift สำหรับการประกาศ MyMacrosPlugin.swift สำหรับการนำไปใช้ และ MyMacrosTests.swift สำหรับการทดสอบ การแยกแบบนี้ช่วยให้บำรุงรักษาง่ายขึ้น

การสร้างมาโครนิพจน์

มาโครนิพจน์สร้างค่าที่นำไปใช้ได้ทันทีในโค้ด ต่อไปนี้คือวิธีสร้างมาโคร #unwrap ที่ใช้แกะค่า optional พร้อมข้อความผิดพลาดที่กำหนดเองและรวมชื่อตัวแปรไว้ด้วย

การประกาศมาโคร

MyMacros.swiftswift
import Foundation

/// Macro that unwraps an optional with an explicit error message
/// Usage: let value = #unwrap(optionalValue)
/// Expansion: guard let optionalValue else { fatalError("...") }; optionalValue
@freestanding(expression)
public macro unwrap<T>(_ value: T?) -> T = #externalMacro(
    module: "MyMacrosPlugin",
    type: "UnwrapMacro"
)

ลายเซ็นระบุว่ามาโครรับค่า optional และส่งคืนค่าที่ไม่ใช่ optional #externalMacro ชี้ไปยังการนำไปใช้ในปลั๊กอิน

การนำไปใช้ด้วย swift-syntax

UnwrapMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftCompilerPlugin

public struct UnwrapMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        // Get the first argument passed to the macro
        guard let argument = node.argumentList.first?.expression else {
            throw MacroError.missingArgument
        }

        // Extract the variable name for the error message
        let variableName = argument.description.trimmingCharacters(
            in: .whitespacesAndNewlines
        )

        // Generate the expansion code
        // Uses an immediately-invoked closure to encapsulate the guard
        return """
            {
                guard let value = \(argument) else {
                    fatalError("Failed to unwrap '\\(\(literal: variableName))' - value was nil")
                }
                return value
            }()
            """
    }
}

// Custom errors for macros
enum MacroError: Error, CustomStringConvertible {
    case missingArgument
    case invalidSyntax(String)

    var description: String {
        switch self {
        case .missingArgument:
            return "The macro requires an argument"
        case .invalidSyntax(let message):
            return "Invalid syntax: \(message)"
        }
    }
}

เมท็อด expansion รับโหนด AST ที่แทนการเรียกใช้มาโคร พร้อมบริบทของการคอมไพล์ และส่งคืน ExprSyntax ซึ่งบรรจุโค้ดที่สร้างขึ้น

การลงทะเบียนปลั๊กอิน

MyMacrosPlugin.swiftswift
import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct MyMacrosPlugin: CompilerPlugin {
    // List all macros provided by this plugin
    let providingMacros: [Macro.Type] = [
        UnwrapMacro.self,
        // Add other macros here
    ]
}

จุดเริ่มต้นนี้บอกคอมไพเลอร์เกี่ยวกับมาโครที่ปลั๊กอินให้บริการ

พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การสร้างมาโคร attached แบบ member

มาโคร member เพิ่มสมาชิก (พร็อพเพอร์ตี้, เมท็อด, ชนิดซ้อน) ให้กับชนิดที่มีอยู่แล้ว ต่อไปนี้คือมาโคร @AutoInit ที่สร้าง initializer สำหรับ stored property ทั้งหมดโดยอัตโนมัติ

การประกาศและการนำไปใช้แบบเต็ม

MyMacros.swiftswift
/// Automatically generates an initializer with all stored properties
@attached(member, names: named(init))
public macro AutoInit() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "AutoInitMacro"
)
AutoInitMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

public struct AutoInitMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // Verify the macro is applied to a struct or class
        guard declaration.is(StructDeclSyntax.self) ||
              declaration.is(ClassDeclSyntax.self) else {
            throw MacroError.invalidSyntax(
                "@AutoInit can only be applied to structs and classes"
            )
        }

        // Collect stored properties
        let properties = declaration.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .filter { isStoredProperty($0) }

        // Generate initializer parameters
        let parameters = properties.compactMap { property -> String? in
            guard let binding = property.bindings.first,
                  let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
                  let type = binding.typeAnnotation?.type else {
                return nil
            }

            let name = identifier.identifier.text
            let typeName = type.description.trimmingCharacters(in: .whitespaces)

            // Check if the property has a default value
            if binding.initializer != nil {
                return "\(name): \(typeName) = \(binding.initializer!.value)"
            }
            return "\(name): \(typeName)"
        }

        // Generate assignments in the init body
        let assignments = properties.compactMap { property -> String? in
            guard let binding = property.bindings.first,
                  let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else {
                return nil
            }
            let name = identifier.identifier.text
            return "self.\(name) = \(name)"
        }

        // Build the complete initializer
        let initDecl: DeclSyntax = """
            public init(\(raw: parameters.joined(separator: ", "))) {
                \(raw: assignments.joined(separator: "\n        "))
            }
            """

        return [initDecl]
    }

    // Check if a variable is a stored property (not computed)
    private static func isStoredProperty(_ variable: VariableDeclSyntax) -> Bool {
        guard let binding = variable.bindings.first else { return false }

        // A computed property has an accessor block with get/set
        if let accessor = binding.accessorBlock {
            // If it's a block with explicit accessors, it's computed
            if accessor.accessors.is(AccessorDeclListSyntax.self) {
                return false
            }
        }

        // let or var without accessor = stored property
        return true
    }
}

การใช้งานมาโคร AutoInit

UserModel.swiftswift
@AutoInit
struct User {
    let id: UUID
    var name: String
    var email: String
    var isActive: Bool = true
}

// Automatically generated code:
// public init(id: UUID, name: String, email: String, isActive: Bool = true) {
//     self.id = id
//     self.name = name
//     self.email = email
//     self.isActive = isActive
// }

// Usage
let user = User(id: UUID(), name: "Alice", email: "alice@example.com")
// isActive uses the default value

มาโครนี้ลด boilerplate ของ initializer ซึ่งมีประโยชน์อย่างยิ่งสำหรับโมเดลข้อมูลที่มีพร็อพเพอร์ตี้จำนวนมาก

มาโคร attached peer สำหรับสร้างเวอร์ชัน async

มาโคร peer เพิ่มดีคลาเรชันในระดับเดียวกับดีคลาเรชันที่ถูกตั้งโน้ต ต่อไปนี้คือมาโคร @AddAsync ที่สร้างเวอร์ชัน async ของฟังก์ชันที่อิงกับ completion handler

MyMacros.swiftswift
/// Automatically generates an async version of a function with completion handler
@attached(peer, names: suffixed(Async))
public macro AddAsync() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "AddAsyncMacro"
)
AddAsyncMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

public struct AddAsyncMacro: PeerMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // Verify it's a function
        guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
            throw MacroError.invalidSyntax(
                "@AddAsync requires a function"
            )
        }

        let functionName = funcDecl.name.text
        let asyncFunctionName = "\(functionName)Async"

        // Analyze parameters to find the completion handler
        let parameters = funcDecl.signature.parameterClause.parameters

        // Filter parameters (exclude completion handler)
        var regularParams: [String] = []
        var completionType: String? = nil

        for param in parameters {
            let paramType = param.type.description

            // Detect a completion handler (closure with Result or simple value)
            if paramType.contains("->") && paramType.contains("Void") {
                // Extract the return type from completion
                completionType = extractCompletionReturnType(from: paramType)
            } else {
                let paramName = param.firstName.text
                let paramSecondName = param.secondName?.text
                let label = paramSecondName ?? paramName
                regularParams.append("\(paramName): \(paramType)")
            }
        }

        guard let returnType = completionType else {
            throw MacroError.invalidSyntax(
                "No completion handler found"
            )
        }

        // Generate arguments for internal call
        let callArgs = parameters.dropLast().map { param in
            let name = param.firstName.text
            return "\(name): \(name)"
        }.joined(separator: ", ")

        // Generate the async function
        let asyncFunc: DeclSyntax = """
            func \(raw: asyncFunctionName)(\(raw: regularParams.joined(separator: ", "))) async throws -> \(raw: returnType) {
                try await withCheckedThrowingContinuation { continuation in
                    \(raw: functionName)(\(raw: callArgs.isEmpty ? "" : callArgs + ", ")completion: { result in
                        switch result {
                        case .success(let value):
                            continuation.resume(returning: value)
                        case .failure(let error):
                            continuation.resume(throwing: error)
                        }
                    })
                }
            }
            """

        return [asyncFunc]
    }

    // Extract return type from a Result type
    private static func extractCompletionReturnType(from type: String) -> String {
        // Simplified pattern - in production, use the AST
        if let match = type.range(of: #"Result<([^,]+)"#, options: .regularExpression) {
            var result = String(type[match])
            result = result.replacingOccurrences(of: "Result<", with: "")
            return result.trimmingCharacters(in: .whitespaces)
        }
        return "Void"
    }
}

สาธิตการใช้มาโคร AddAsync

NetworkService.swiftswift
class NetworkService {
    @AddAsync
    func fetchUser(
        id: Int,
        completion: @escaping (Result<User, Error>) -> Void
    ) {
        // Implementation with callback
        URLSession.shared.dataTask(with: URL(string: "/users/\(id)")!) { data, _, error in
            if let error = error {
                completion(.failure(error))
            } else if let data = data {
                let user = try? JSONDecoder().decode(User.self, from: data)
                completion(.success(user!))
            }
        }.resume()
    }

    // Automatically generates:
    // func fetchUserAsync(id: Int) async throws -> User {
    //     try await withCheckedThrowingContinuation { continuation in
    //         fetchUser(id: id, completion: { result in
    //             switch result {
    //             case .success(let value):
    //                 continuation.resume(returning: value)
    //             case .failure(let error):
    //                 continuation.resume(throwing: error)
    //             }
    //         })
    //     }
    // }
}

// Modern usage with async/await
let user = try await networkService.fetchUserAsync(id: 42)
ข้อจำกัดของมาโคร peer

ชื่อฟังก์ชันที่สร้างขึ้นต้องประกาศใน names: ของแอตทริบิวต์ @attached ในที่นี้ suffixed(Async) หมายความว่าฟังก์ชันที่สร้างใหม่จะนำคำต่อท้าย "Async" ต่อท้ายชื่อเดิม

การทดสอบยูนิตสำหรับมาโคร

การทดสอบมาโครเป็นเรื่องสำคัญ เพราะมาโครสร้างโค้ดที่จะถูกคอมไพล์ในภายหลัง Swift มี SwiftSyntaxMacrosTestSupport เพื่อช่วยให้การทดสอบลักษณะนี้ง่ายขึ้น

MyMacrosTests.swiftswift
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import MyMacrosPlugin

final class MyMacrosTests: XCTestCase {

    // Dictionary of macros to test
    let testMacros: [String: Macro.Type] = [
        "unwrap": UnwrapMacro.self,
        "AutoInit": AutoInitMacro.self,
        "AddAsync": AddAsyncMacro.self
    ]

    func testUnwrapMacroExpansion() throws {
        assertMacroExpansion(
            """
            let value = #unwrap(optionalString)
            """,
            expandedSource: """
            let value = {
                guard let value = optionalString else {
                    fatalError("Failed to unwrap 'optionalString' - value was nil")
                }
                return value
            }()
            """,
            macros: testMacros
        )
    }

    func testAutoInitMacroWithStruct() throws {
        assertMacroExpansion(
            """
            @AutoInit
            struct Point {
                let x: Int
                var y: Int
            }
            """,
            expandedSource: """
            struct Point {
                let x: Int
                var y: Int

                public init(x: Int, y: Int) {
                    self.x = x
                    self.y = y
                }
            }
            """,
            macros: testMacros
        )
    }

    func testAutoInitWithDefaultValues() throws {
        assertMacroExpansion(
            """
            @AutoInit
            struct Config {
                var timeout: Int = 30
                var retryCount: Int
            }
            """,
            expandedSource: """
            struct Config {
                var timeout: Int = 30
                var retryCount: Int

                public init(timeout: Int = 30, retryCount: Int) {
                    self.timeout = timeout
                    self.retryCount = retryCount
                }
            }
            """,
            macros: testMacros
        )
    }

    func testAutoInitFailsOnEnum() throws {
        assertMacroExpansion(
            """
            @AutoInit
            enum Status {
                case active
            }
            """,
            expandedSource: """
            enum Status {
                case active
            }
            """,
            diagnostics: [
                DiagnosticSpec(
                    message: "@AutoInit can only be applied to structs and classes",
                    line: 1,
                    column: 1
                )
            ],
            macros: testMacros
        )
    }
}

การทดสอบยืนยันว่ามาโครขยายโค้ดอย่างถูกต้องและให้ข้อความผิดพลาดที่เหมาะสมเมื่อใช้งานไม่ถูกต้อง

พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

มาโครขั้นสูง: property wrapper แบบ observable

มาโครนี้ผสมหลายบทบาทเข้าด้วยกันเพื่อสร้างระบบติดตามพร็อพเพอร์ตี้พร้อมการแจ้งเตือนอัตโนมัติ

MyMacros.swiftswift
/// Adds automatic property change observation
@attached(accessor)
@attached(peer, names: prefixed(_))
public macro Observable() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "ObservableMacro"
)
ObservableMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

// Implements both roles: accessor and peer
public enum ObservableMacro {}

extension ObservableMacro: AccessorMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingAccessorsOf declaration: some DeclSyntax,
        in context: some MacroExpansionContext
    ) throws -> [AccessorDeclSyntax] {
        guard let varDecl = declaration.as(VariableDeclSyntax.self),
              let binding = varDecl.bindings.first,
              let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else {
            return []
        }

        let name = identifier.identifier.text
        let storageName = "_\(name)"

        // Generate get and set accessors
        let getter: AccessorDeclSyntax = """
            get {
                access(keyPath: \\.\(raw: name))
                return \(raw: storageName)
            }
            """

        let setter: AccessorDeclSyntax = """
            set {
                withMutation(keyPath: \\.\(raw: name)) {
                    \(raw: storageName) = newValue
                }
            }
            """

        return [getter, setter]
    }
}

extension ObservableMacro: PeerMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard let varDecl = declaration.as(VariableDeclSyntax.self),
              let binding = varDecl.bindings.first,
              let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
              let type = binding.typeAnnotation?.type else {
            return []
        }

        let name = identifier.identifier.text
        let storageName = "_\(name)"
        let typeName = type.description

        // Generate private storage property
        let initializer = binding.initializer.map { " \($0)" } ?? ""
        let storageDecl: DeclSyntax = """
            private var \(raw: storageName): \(raw: typeName)\(raw: initializer)
            """

        return [storageDecl]
    }
}

การใช้รูปแบบ Observable

ViewModel.swiftswift
@Observable
class UserViewModel {
    @Observable var name: String = ""
    @Observable var age: Int = 0
    @Observable var isActive: Bool = true

    // Generated code for each property:
    // private var _name: String = ""
    // var name: String {
    //     get {
    //         access(keyPath: \.name)
    //         return _name
    //     }
    //     set {
    //         withMutation(keyPath: \.name) {
    //             _name = newValue
    //         }
    //     }
    // }
}

Apple ใช้รูปแบบเดียวกันนี้ในเฟรมเวิร์ก Observation ใหม่ตั้งแต่ Swift 5.9

การดีบักและตรวจสอบมาโคร

Xcode มีเครื่องมือหลายอย่างที่ช่วยดีบักมาโครและทำความเข้าใจโค้ดที่สร้างขึ้น

การขยายใน Xcode

DebuggingMacros.swiftswift
// Right-click on macro call → "Expand Macro"
// Displays generated code inline

@AutoInit
struct Product {
    let id: UUID
    var name: String
    var price: Decimal
}

// To see the expansion:
// 1. Right-click on @AutoInit
// 2. Select "Expand Macro"
// 3. Generated code displays inline for inspection and debugging

การบันทึกล็อกระหว่างการพัฒนา

DebugMacro.swiftswift
public struct DebugMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        // Print the node's AST to understand the structure
        print("=== DEBUG MACRO ===")
        print("Node: \(node)")
        print("Arguments: \(node.argumentList)")

        // Complete dump of the syntax tree
        dump(node)

        // Continue with normal expansion
        return "42"
    }
}

สำรวจ AST ด้วย swift-ast-explorer

เครื่องมือออนไลน์ swift-ast-explorer.com ใช้แสดงต้นไม้ไวยากรณ์ของโค้ด Swift ใด ๆ ก็ได้ และเป็นเครื่องมือสำคัญที่ช่วยให้เข้าใจวิธีเดินทางผ่านโหนด AST เมื่อต้องสร้างมาโคร

แนวทางปฏิบัติที่ดีสำหรับ Swift Macros

การสร้างมาโครที่ดูแลรักษาง่ายต้องอาศัยการปฏิบัติตามแนวทางบางประการและการหลีกเลี่ยงข้อผิดพลาดที่พบบ่อย

การตรวจสอบและข้อความผิดพลาด

ValidationBestPractices.swiftswift
public struct ValidatedMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // ✅ Validate usage context
        guard declaration.is(StructDeclSyntax.self) else {
            // ✅ Clear error messages with possible localization
            context.diagnose(
                Diagnostic(
                    node: node,
                    message: MacroDiagnosticMessage(
                        id: "invalid-target",
                        message: "This macro can only be applied to structs",
                        severity: .error
                    )
                )
            )
            return []
        }

        // ✅ Check required arguments
        guard let arguments = node.arguments else {
            context.diagnose(
                Diagnostic(
                    node: node,
                    message: MacroDiagnosticMessage(
                        id: "missing-args",
                        message: "Required arguments missing",
                        severity: .error
                    )
                )
            )
            return []
        }

        // Implementation...
        return []
    }
}

// Structure for diagnostic messages
struct MacroDiagnosticMessage: DiagnosticMessage {
    let id: String
    let message: String
    let severity: DiagnosticSeverity

    var diagnosticID: MessageID {
        MessageID(domain: "MyMacros", id: id)
    }
}

การสร้างโค้ดที่อ่านง่าย

ReadableCodeGeneration.swiftswift
// ❌ Hard-to-read generated code
let badCode: DeclSyntax = "public init(a:Int,b:String,c:Bool){self.a=a;self.b=b;self.c=c}"

// ✅ Properly formatted generated code
let goodCode: DeclSyntax = """
    public init(
        a: Int,
        b: String,
        c: Bool
    ) {
        self.a = a
        self.b = b
        self.c = c
    }
    """

โค้ดที่สร้างขึ้นควรอ่านง่ายเทียบเท่าโค้ดที่เขียนด้วยมือ เพราะนักพัฒนาจะตรวจดูผ่าน "Expand Macro"

บทสรุป

Swift Macros เป็นเครื่องมือที่ทรงพลังในการกำจัด boilerplate โดยไม่สูญเสียความปลอดภัยของชนิดข้อมูลแบบสแตติก เทคโนโลยีนี้ช่วยให้:

ประเด็นสำคัญ:

  • ✅ มาโครสองประเภท: freestanding (#) และ attached (@)
  • ✅ ห้าบทบาทของ attached: peer, accessor, member, memberAttribute, conformance
  • ✅ การนำไปใช้ผ่าน swift-syntax และการจัดการ AST
  • ✅ การทดสอบที่จำเป็นด้วย SwiftSyntaxMacrosTestSupport
  • ✅ ต้องมีแพ็กเกจแยกต่างหากสำหรับการนำไปใช้
  • ✅ การดีบักผ่าน "Expand Macro" ใน Xcode
  • ✅ ข้อความผิดพลาดที่ชัดเจนเป็นปัจจัยสำคัญสำหรับประสบการณ์นักพัฒนา

Swift Macros มีประโยชน์เป็นพิเศษในการสร้าง conformance (Equatable, Codable), property wrapper ขั้นสูง และการปรับ API ที่อิง callback ให้รองรับ async/await

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#swift
#ios
#macros
#metaprogramming
#swift-syntax

แชร์

บทความที่เกี่ยวข้อง

การย้ายจาก Combine ไปยัง async/await ใน Swift พร้อมรูปแบบการอยู่ร่วมกัน

Combine vs async/await ใน Swift: รูปแบบการย้ายระบบแบบค่อยเป็นค่อยไป

คู่มือฉบับสมบูรณ์สำหรับการย้ายจาก Combine ไปยัง async/await ใน Swift: กลยุทธ์แบบค่อยเป็นค่อยไป รูปแบบการเชื่อมโยง และการอยู่ร่วมกันของกระบวนทัศน์ในโค้ดเบส iOS

คำถามสัมภาษณ์การเข้าถึง iOS: VoiceOver และ Dynamic Type

คำถามสัมภาษณ์การเข้าถึง iOS ในปี 2026: VoiceOver และ Dynamic Type

เตรียมตัวสัมภาษณ์ iOS ด้วยคำถามสำคัญเรื่องการเข้าถึง: VoiceOver, Dynamic Type, traits เชิงความหมาย และการตรวจสอบ.

สถาปัตยกรรมการสมัครสมาชิก iOS StoreKit 2 และการตรวจสอบใบเสร็จ

การสัมภาษณ์ StoreKit 2: การจัดการการสมัครสมาชิกและการตรวจสอบใบเสร็จ

เชี่ยวชาญคำถามสัมภาษณ์ iOS เกี่ยวกับ StoreKit 2 การจัดการการสมัครสมาชิก การตรวจสอบใบเสร็จ และการนำการซื้อในแอปไปใช้ พร้อมตัวอย่างโค้ด Swift ที่ใช้งานได้จริง