Swift Macros: praktyczne przykłady metaprogramowania

Kompletny przewodnik po Swift Macros: tworzenie makr freestanding i attached, w tym body macros ze Swift 6, manipulacja AST przy użyciu swift-syntax oraz praktyczne przykłady eliminujące powtarzalny kod.

Metaprogramowanie ze Swift Macros i praktycznymi przykładami kodu

Swift Macros zmieniają sposób, w jaki programiści piszą i utrzymują kod. Wprowadzone w Swift 5.9 i znacząco rozbudowane w Swift 6, makra umożliwiają generowanie kodu w czasie kompilacji przy zachowaniu statycznej kontroli typów. W odróżnieniu od makr preprocesora języka C, Swift Macros są bezpieczne typowo, zintegrowane z kompilatorem i w pełni wspierane przez narzędzia deweloperskie, w tym funkcję "Expand Macro" w Xcode.

Co obejmuje ten przewodnik

Ten przewodnik omawia tworzenie Swift Macros od podstawowych koncepcji po zaawansowane implementacje, w tym nowe body macros wprowadzone w Swift 6 (SE-0415). Wszystkie przykłady używają Swift 6 i swift-syntax 600.0.0+.

Rodzaje Swift Macros

Swift udostępnia dwie główne kategorie makr, z których każda ma swoje zastosowania. Makra freestanding działają samodzielnie jako wyrażenia lub deklaracje, natomiast makra attached przypinają się do istniejących deklaracji, modyfikując je lub rozszerzając.

Makra freestanding: wyrażenie i deklaracja

Makra freestanding zaczynają się od symbolu # i mogą zwracać wartość (wyrażenie) lub tworzyć nowe deklaracje. Oto konkretny przykład makra wyrażeniowego:

MacroUsage.swiftswift
// Freestanding expression macro - generates a value
let buildInfo = #buildDate
// Expansion → "2026-08-23 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

Kluczowa różnica między wyrażeniem a deklaracją tkwi w wyniku: wyrażenie zwraca wartość, natomiast deklaracja generuje kod strukturalny (typy, funkcje, zmienne).

Makra attached: sześć ról

Makra attached zaczynają się od @ i występują przed deklaracją. Swift 6 definiuje sześć różnych ról dla tych makr, w tym nową rolę body dla implementacji funkcji:

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(extension) - adds protocol conformances
@Hashable
struct User {
    var id: Int
    var name: String
}
// Expansion → adds extension User: Hashable { ... }

// @attached(body) - NEW in Swift 6: synthesizes function bodies
@Remote
func fetchData(endpoint: String) -> Data
// Expansion → generates the entire function implementation

Role te można łączyć, aby tworzyć potężne makra przekształcające kod jednocześnie w wielu wymiarach.

Swift 6 Body Macros: SE-0415

Swift 6 wprowadził body macros poprzez SE-0415, umożliwiając makrom syntetyzowanie lub zastępowanie implementacji funkcji. Adresuje to przypadki użycia, których żadna wcześniejsza rola makra nie mogła obsłużyć.

Protokół BodyMacro

Protokół BodyMacro pozwala na całkowite zastąpienie ciała funkcji:

BodyMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

public protocol BodyMacro: AttachedMacro {
    static func expansion(
        of node: AttributeSyntax,
        providingBodyFor declaration: some DeclSyntaxProtocol &
            WithOptionalCodeBlockSyntax,
        in context: some MacroExpansionContext
    ) throws -> [CodeBlockItemSyntax]
}

Makro otrzymuje oryginalną deklarację i zwraca elementy bloku kodu, które stają się ciałem funkcji. Każde istniejące ciało zostaje zastąpione.

Implementacja makra Remote Procedure Call

Body macros są szczególnie przydatne do generowania kodu warstwy sieciowej na podstawie sygnatur funkcji:

RemoteMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

public struct RemoteMacro: BodyMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingBodyFor declaration: some DeclSyntaxProtocol &
            WithOptionalCodeBlockSyntax,
        in context: some MacroExpansionContext
    ) throws -> [CodeBlockItemSyntax] {
        guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
            throw MacroError.invalidSyntax("@Remote requires a function")
        }
        
        let functionName = funcDecl.name.text
        let parameters = funcDecl.signature.parameterClause.parameters
        
        // Generate parameter encoding
        let paramDict = parameters.map { param in
            let name = param.firstName.text
            return "\"\(name)\": \(name)"
        }.joined(separator: ", ")
        
        // Generate the RPC body
        return [
            """
            let params: [String: Any] = [\(raw: paramDict)]
            return try await RPCClient.shared.call(
                method: "\(raw: functionName)",
                params: params
            )
            """
        ]
    }
}

Deklaracja i użycie

MyMacros.swiftswift
/// Generates a remote procedure call implementation
@attached(body)
public macro Remote() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "RemoteMacro"
)
APIService.swiftswift
class APIService {
    @Remote
    func createUser(name: String, email: String) async throws -> User
    
    @Remote
    func deleteUser(id: UUID) async throws -> Bool
    
    // Each function body is generated at compile time:
    // func createUser(name: String, email: String) async throws -> User {
    //     let params: [String: Any] = ["name": name, "email": email]
    //     return try await RPCClient.shared.call(
    //         method: "createUser",
    //         params: params
    //     )
    // }
}
Ograniczenie body macro

Do danej funkcji można zastosować co najwyżej jedno body macro. Dla komponowalnego wstrzykiwania preambuły (logowanie, śledzenie) protokół PreambleMacro jest rozważany w przyszłych wersjach Swift.

Konfiguracja projektu do tworzenia makr

Tworzenie Swift Macros wymaga pakietu Swift Package o określonej strukturze. Pakiet zależy od swift-syntax, oficjalnej biblioteki służącej do manipulowania kodem Swift jako abstrakcyjnym drzewem składniowym (AST).

Struktura Package.swift

Package.swiftswift
// swift-tools-version: 6.0
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 - use 600.0.0+ for Swift 6
        .package(
            url: "https://github.com/swiftlang/swift-syntax.git",
            from: "600.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")
            ]
        )
    ]
)

Konfiguracja wyraźnie oddziela deklaracje makr (widoczne dla kodu klienta) od ich implementacji (uruchamianej w czasie kompilacji).

Zalecana organizacja

Potrzebne są co najmniej trzy pliki: MyMacros.swift dla deklaracji, MyMacrosPlugin.swift dla implementacji oraz MyMacrosTests.swift dla testów. Taki podział ułatwia utrzymanie projektu.

Tworzenie makra wyrażeniowego

Makra wyrażeniowe generują wartość, którą można od razu wykorzystać w kodzie. Poniżej przykład makra #unwrap rozpakowującego opcjonalną wartość, z komunikatem błędu zawierającym nazwę zmiennej. Więcej o wzorcach obsługi błędów w Swift można znaleźć w przewodniku o Swift structured concurrency.

Deklaracja makra

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"
)

Sygnatura określa, że makro przyjmuje wartość opcjonalną i zwraca jej wersję nieopcjonalną. #externalMacro wskazuje na implementację znajdującą się w pluginie.

Implementacja przy użyciu 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)"
        }
    }
}

Metoda expansion otrzymuje węzeł AST reprezentujący wywołanie makra oraz kontekst kompilacji. Zwraca ExprSyntax zawierający wygenerowany kod.

Rejestracja pluginu

MyMacrosPlugin.swiftswift
import SwiftCompilerPlugin
import SwiftSyntaxMacros

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

Ten punkt wejścia informuje kompilator o makrach dostępnych w pluginie.

Gotowy na rozmowy o iOS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Tworzenie makra attached typu member

Makra typu member dodają nowe składowe (właściwości, metody, typy zagnieżdżone) do istniejącego typu. Poniżej makro @AutoInit, które automatycznie generuje inicjalizator obejmujący wszystkie składowe.

Pełna deklaracja i implementacja

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
    }
}

Użycie makra 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

To makro eliminuje boilerplate inicjalizatora, co jest szczególnie przydatne w modelach danych z wieloma właściwościami.

Makro attached peer do generowania wersji async

Makra peer dodają deklaracje na tym samym poziomie co adnotowana deklaracja. Poniżej makro @AddAsync generujące async-wersję funkcji opartej na completion handlerze. Ten wzorzec jest szczególnie istotny przy migracji starszego kodu sieciowego, jak opisano w przewodniku o migracji Combine vs async/await.

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"
    }
}

Demonstracja makra 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)
Ograniczenia makr peer

Nazwa generowanej funkcji musi być zadeklarowana w names: atrybutu @attached. W tym przypadku suffixed(Async) informuje, że wygenerowana funkcja otrzyma sufiks "Async" dodany do oryginalnej nazwy.

Testy jednostkowe makr

Testowanie makr jest niezbędne, ponieważ generują one kod, który następnie zostanie skompilowany. Swift udostępnia SwiftSyntaxMacrosTestSupport, aby ułatwić takie testy. Począwszy od swift-syntax 603.0.0+, Swift Testing jest również wspierany obok XCTest.

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,
        "Remote": RemoteMacro.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
        )
    }

    func testRemoteMacroExpansion() throws {
        assertMacroExpansion(
            """
            @Remote
            func fetchUser(id: Int) async throws -> User
            """,
            expandedSource: """
            func fetchUser(id: Int) async throws -> User {
                let params: [String: Any] = ["id": id]
                return try await RPCClient.shared.call(
                    method: "fetchUser",
                    params: params
                )
            }
            """,
            macros: testMacros
        )
    }
}

Testy weryfikują poprawną ekspansję kodu oraz odpowiednie komunikaty błędów przy niewłaściwym użyciu.

Debugowanie i analizowanie makr

Xcode oferuje kilka narzędzi do debugowania makr i zrozumienia generowanego kodu.

Ekspansja w 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

Logowanie podczas tworzenia

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"
    }
}

Eksploracja AST za pomocą swift-ast-explorer

Online'owe narzędzie swift-ast-explorer.com umożliwia podgląd drzewa składniowego dowolnego kodu Swift. Zrozumienie struktury węzłów AST jest kluczowe podczas implementowania makr, co pokazano również w przewodniku po frameworku Swift Testing, który omawia makra testowe wbudowane w Swift.

Dobre praktyki Swift Macros

Tworzenie łatwych w utrzymaniu makr wymaga przestrzegania kilku zasad i unikania typowych pułapek.

Walidacja i komunikaty błędów

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)
    }
}

Generowanie czytelnego kodu

ReadableCodeGeneration.swiftswift
// Avoid: 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}"

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

Generowany kod powinien być równie czytelny jak kod pisany ręcznie, ponieważ deweloperzy będą go analizować przez "Expand Macro".

Źródła

Co umożliwiają Swift Macros w praktyce

Swift Macros stanowią potężne narzędzie do eliminowania boilerplate'u przy zachowaniu statycznej kontroli typów. Technologia ta umożliwia:

  • Dwie kategorie makr: freestanding (#) i attached (@)
  • Sześć ról attached w Swift 6: peer, accessor, member, memberAttribute, extension oraz nową rolę body
  • Implementację za pomocą swift-syntax 600.0.0+ i manipulacji AST
  • Obowiązkowe testy z SwiftSyntaxMacrosTestSupport, teraz z obsługą Swift Testing
  • Wymagany osobny pakiet dla implementacji
  • Debugowanie przez "Expand Macro" w Xcode
  • Czytelne komunikaty błędów kluczowe dla doświadczenia deweloperskiego

Swift Macros są szczególnie przydatne do generowania zgodności (Equatable, Codable), tworzenia zaawansowanych property wrapperów, syntetyzowania implementacji RPC na podstawie sygnatur oraz modernizacji API opartych na callbackach w kierunku async/await.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Wyzwanie dnia

Znajdziesz błąd w iOS?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 23 sierpnia 2026

Tagi

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

Udostępnij

Powiązane artykuły