Swift Macros: Contoh Praktis Metaprogramming

Panduan lengkap Swift Macros: pembuatan macro freestanding dan attached termasuk body macro Swift 6, manipulasi AST dengan swift-syntax, serta contoh praktis untuk menghilangkan kode berulang.

Metaprogramming dengan Swift Macros dan contoh kode praktis

Swift Macros mengubah cara pengembang menulis dan merawat kode. Diperkenalkan dengan Swift 5.9 dan diperluas secara signifikan di Swift 6, macro memungkinkan pembuatan kode pada saat kompilasi sambil mempertahankan keamanan tipe statis. Berbeda dengan macro preprocessor C, Swift Macros aman secara tipe, terintegrasi dengan compiler, dan didukung penuh oleh tooling pengembangan termasuk fitur "Expand Macro" Xcode.

Cakupan panduan ini

Panduan ini mengupas pembuatan Swift Macros dari konsep dasar hingga implementasi tingkat lanjut, termasuk body macro baru yang diperkenalkan di Swift 6 (SE-0415). Semua contoh menggunakan Swift 6 dan swift-syntax 600.0.0+.

Memahami jenis Swift Macros

Swift menyediakan dua kategori utama macro, masing-masing dengan kasus pakai berbeda. Macro freestanding bekerja secara mandiri sebagai ekspresi atau deklarasi, sedangkan macro attached terikat pada deklarasi yang sudah ada untuk memodifikasi atau memperkayanya.

Macro freestanding: ekspresi dan deklarasi

Macro freestanding diawali dengan simbol # dan dapat mengembalikan nilai (ekspresi) atau membuat deklarasi baru. Berikut contoh konkret macro ekspresi:

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

Perbedaan mendasar antara ekspresi dan deklarasi terletak pada hasilnya: ekspresi menghasilkan nilai, sedangkan deklarasi menghasilkan kode struktural (tipe, fungsi, variabel).

Macro attached: enam peran

Macro attached diawali @ dan ditempatkan di depan sebuah deklarasi. Swift 6 mendefinisikan enam peran berbeda untuk macro ini, termasuk peran body baru untuk implementasi fungsi:

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

Peran-peran tersebut dapat dikombinasikan untuk membentuk macro tangguh yang mengubah kode pada beberapa dimensi sekaligus.

Swift 6 Body Macros: SE-0415

Swift 6 memperkenalkan function body macros melalui SE-0415, yang memungkinkan macro untuk mensintesis atau mengganti implementasi fungsi. Ini menangani kasus pakai yang tidak dapat ditangani oleh peran macro sebelumnya.

Protokol BodyMacro

Protokol BodyMacro memungkinkan penggantian body fungsi secara lengkap:

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

Macro menerima deklarasi asli dan mengembalikan item blok kode yang menjadi body fungsi. Body yang sudah ada akan diganti.

Implementasi Remote Procedure Call Macro

Body macro sangat berguna untuk menghasilkan kode network layer dari signature fungsi:

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

Deklarasi dan penggunaan

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
    //     )
    // }
}
Batasan body macro

Hanya satu body macro yang dapat diterapkan pada suatu fungsi. Untuk composable preamble injection (logging, tracing), protokol PreambleMacro sedang dipertimbangkan di versi Swift mendatang.

Penyiapan proyek untuk membuat macro

Membuat Swift Macros memerlukan Swift Package dengan struktur tertentu. Paket bergantung pada swift-syntax, pustaka resmi untuk memanipulasi kode Swift sebagai abstract syntax tree (AST).

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

Konfigurasi ini memisahkan deklarasi macro (yang dilihat kode klien) dari implementasinya (yang dijalankan saat kompilasi) secara jelas.

Organisasi yang disarankan

Minimal tiga berkas dibutuhkan: MyMacros.swift untuk deklarasi, MyMacrosPlugin.swift untuk implementasi, dan MyMacrosTests.swift untuk pengujian. Pemisahan ini memudahkan perawatan.

Membuat macro ekspresi

Macro ekspresi menghasilkan nilai yang langsung dipakai dalam kode. Berikut contoh pembuatan macro #unwrap yang membuka optional dengan pesan kesalahan kustom yang menyertakan nama variabel. Untuk lebih lanjut tentang pola penanganan error Swift, lihat panduan tentang Swift structured concurrency.

Deklarasi macro

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

Tanda tangan menyatakan bahwa macro menerima sebuah optional dan mengembalikan nilai non-optional. #externalMacro mengarah ke implementasi di dalam plugin.

Implementasi dengan 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)"
        }
    }
}

Metode expansion menerima node AST yang merepresentasikan pemanggilan macro dan konteks kompilasi. Metode tersebut mengembalikan ExprSyntax berisi kode yang dihasilkan.

Pendaftaran plugin

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

Titik masuk ini memberi tahu compiler tentang macro yang tersedia di dalam plugin.

Siap menguasai wawancara iOS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Membuat macro attached jenis member

Macro member menambahkan anggota (properti, metode, tipe bersarang) ke tipe yang sudah ada. Berikut macro @AutoInit yang otomatis menghasilkan inisialisasi dengan seluruh stored property.

Deklarasi dan implementasi lengkap

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

Penggunaan macro 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

Macro ini meniadakan boilerplate inisialisasi, sangat berguna pada model data dengan banyak properti.

Macro attached peer untuk membuat versi async

Macro peer menambahkan deklarasi di tingkat yang sama dengan deklarasi yang dianotasi. Berikut macro @AddAsync yang membuat versi async dari fungsi berbasis completion handler. Pola ini sangat relevan saat memigrasi kode networking legacy, seperti yang dibahas dalam panduan migrasi 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"
    }
}

Demonstrasi macro 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)
Batasan macro peer

Nama fungsi yang dihasilkan harus dideklarasikan pada names: atribut @attached. Di sini suffixed(Async) menunjukkan bahwa fungsi yang dihasilkan akan menambahkan akhiran "Async" pada nama aslinya.

Pengujian unit untuk macro

Menguji macro sangat penting karena macro menghasilkan kode yang akan dikompilasi. Swift menyediakan SwiftSyntaxMacrosTestSupport untuk memudahkan pengujian semacam ini. Sejak swift-syntax 603.0.0+, Swift Testing juga didukung bersamaan dengan 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
        )
    }
}

Pengujian ini memverifikasi ekspansi kode yang benar dan pesan kesalahan yang sesuai untuk penggunaan tidak valid.

Debugging dan inspeksi macro

Xcode menyediakan beberapa alat untuk men-debug macro dan memahami kode yang dihasilkan.

Ekspansi di 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

Logging selama pengembangan

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

Menjelajahi AST dengan swift-ast-explorer

Alat daring swift-ast-explorer.com memungkinkan visualisasi pohon sintaksis untuk kode Swift apa pun. Memahami struktur node AST sangat penting saat mengimplementasikan macro, seperti yang didemonstrasikan dalam panduan framework Swift Testing yang mencakup testing macro bawaan Swift.

Praktik terbaik Swift Macros

Membuat macro yang mudah dirawat membutuhkan kepatuhan pada konvensi tertentu serta menghindari jebakan umum.

Validasi dan pesan kesalahan

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

Menghasilkan kode yang mudah dibaca

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

Kode yang dihasilkan harus seenak kode yang ditulis tangan, sebab pengembang akan memeriksanya melalui "Expand Macro".

Sumber

Kemampuan Swift Macros dalam praktik

Swift Macros merupakan alat ampuh untuk menghilangkan boilerplate sambil mempertahankan keamanan tipe statis. Teknologi ini memungkinkan:

  • Dua kategori macro: freestanding (#) dan attached (@)
  • Enam peran attached di Swift 6: peer, accessor, member, memberAttribute, extension, dan peran body yang baru
  • Implementasi via swift-syntax 600.0.0+ dan manipulasi AST
  • Pengujian wajib dengan SwiftSyntaxMacrosTestSupport, kini mendukung Swift Testing
  • Paket terpisah diperlukan untuk implementasi
  • Debugging via "Expand Macro" di Xcode
  • Pesan kesalahan eksplisit penting untuk pengalaman pengembang

Swift Macros sangat berguna untuk membuat conformance (Equatable, Codable), property wrapper tingkat lanjut, mensintesis implementasi RPC dari signature, dan modernisasi API berbasis callback ke async/await.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tantangan harian

Bisakah kamu menemukan bug di iOS?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 23 Agustus 2026

Tag

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

Bagikan

Artikel terkait