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

Swift Macros ที่เปิดตัวพร้อมกับ Swift 5.9 และ Xcode 15 ถือเป็นการปฏิวัติวิธีเขียนโค้ด Swift โดยอนุญาตให้สร้างโค้ดในขั้นตอนคอมไพล์ ทำให้ลด boilerplate ลงโดยไม่สูญเสียความปลอดภัยของชนิดข้อมูลแบบสแตติก ต่างจากมาโครของพรีโพรเซสเซอร์ใน C ตรงที่ Swift Macros ปลอดภัยทางชนิด ผูกกับคอมไพเลอร์ และได้รับการสนับสนุนเต็มรูปแบบจากเครื่องมือพัฒนา
คู่มือฉบับนี้พาสำรวจการสร้าง Swift Macros ตั้งแต่ต้นจนจบ ตั้งแต่แนวคิดพื้นฐานไปจนถึงการนำไปใช้งานขั้นสูง พร้อมตัวอย่างโค้ดที่ใช้งานได้จริงและพร้อมปรับใช้กับโปรเจกต์ iOS ใด ๆ
ทำความเข้าใจประเภทของ Swift Macros
Swift มีมาโครหลักสองประเภท แต่ละประเภทรองรับสถานการณ์การใช้งานที่ต่างกัน มาโครแบบ freestanding ทำงานเป็นนิพจน์หรือดีคลาเรชันโดยอิสระ ในขณะที่มาโคร attached จะแนบกับดีคลาเรชันเดิมเพื่อปรับเปลี่ยนหรือเสริมความสามารถ
มาโคร freestanding: นิพจน์และดีคลาเรชัน
มาโคร freestanding เริ่มต้นด้วยสัญลักษณ์ # และสามารถส่งคืนค่า (นิพจน์) หรือสร้างดีคลาเรชันใหม่ได้ ต่อไปนี้คือตัวอย่างเชิงรูปธรรมของมาโครนิพจน์:
// 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 กำหนดบทบาทห้าแบบสำหรับมาโครเหล่านี้:
// @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
// 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 พร้อมข้อความผิดพลาดที่กำหนดเองและรวมชื่อตัวแปรไว้ด้วย
การประกาศมาโคร
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
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 ซึ่งบรรจุโค้ดที่สร้างขึ้น
การลงทะเบียนปลั๊กอิน
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 ทั้งหมดโดยอัตโนมัติ
การประกาศและการนำไปใช้แบบเต็ม
/// Automatically generates an initializer with all stored properties
@attached(member, names: named(init))
public macro AutoInit() = #externalMacro(
module: "MyMacrosPlugin",
type: "AutoInitMacro"
)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
@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
/// Automatically generates an async version of a function with completion handler
@attached(peer, names: suffixed(Async))
public macro AddAsync() = #externalMacro(
module: "MyMacrosPlugin",
type: "AddAsyncMacro"
)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
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)ชื่อฟังก์ชันที่สร้างขึ้นต้องประกาศใน names: ของแอตทริบิวต์ @attached ในที่นี้ suffixed(Async) หมายความว่าฟังก์ชันที่สร้างใหม่จะนำคำต่อท้าย "Async" ต่อท้ายชื่อเดิม
การทดสอบยูนิตสำหรับมาโคร
การทดสอบมาโครเป็นเรื่องสำคัญ เพราะมาโครสร้างโค้ดที่จะถูกคอมไพล์ในภายหลัง Swift มี SwiftSyntaxMacrosTestSupport เพื่อช่วยให้การทดสอบลักษณะนี้ง่ายขึ้น
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
มาโครนี้ผสมหลายบทบาทเข้าด้วยกันเพื่อสร้างระบบติดตามพร็อพเพอร์ตี้พร้อมการแจ้งเตือนอัตโนมัติ
/// Adds automatic property change observation
@attached(accessor)
@attached(peer, names: prefixed(_))
public macro Observable() = #externalMacro(
module: "MyMacrosPlugin",
type: "ObservableMacro"
)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
@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
// 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การบันทึกล็อกระหว่างการพัฒนา
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
การสร้างมาโครที่ดูแลรักษาง่ายต้องอาศัยการปฏิบัติตามแนวทางบางประการและการหลีกเลี่ยงข้อผิดพลาดที่พบบ่อย
การตรวจสอบและข้อความผิดพลาด
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)
}
}การสร้างโค้ดที่อ่านง่าย
// ❌ 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
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

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

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

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