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

Swift Macros, wprowadzone wraz ze Swift 5.9 i Xcode 15, stanowią rewolucję w sposobie pisania kodu Swift. Funkcja ta umożliwia generowanie kodu w czasie kompilacji, eliminując boilerplate bez utraty 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.
Ten przewodnik omawia tworzenie Swift Macros od początku do końca: od podstawowych koncepcji po zaawansowane implementacje, z działającymi przykładami kodu gotowymi do zastosowania w dowolnym projekcie iOS.
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:
// 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 pendingKluczowa 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: pięć ról
Makra attached zaczynają się od @ i występują przed deklaracją. Swift definiuje pięć różnych ról dla tych makr:
// @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 { ... }Role te można łączyć, aby tworzyć potężne makra przekształcające kod jednocześnie w wielu wymiarach.
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
// 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")
]
)
]
)Konfiguracja wyraźnie oddziela deklaracje makr (widoczne dla kodu klienta) od ich implementacji (uruchamianej w czasie kompilacji).
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.
Deklaracja makra
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
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
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
]
}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
/// 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
}
}Użycie makra 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 valueTo 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.
/// 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"
}
}Demonstracja makra 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)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.
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
)
}
}Testy weryfikują poprawną ekspansję kodu oraz odpowiednie komunikaty błędów przy niewłaściwym użyciu.
Gotowy na rozmowy o iOS?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Zaawansowane makro: obserwowalny property wrapper
To makro łączy kilka ról, aby zbudować system obserwacji właściwości z automatycznymi powiadomieniami.
/// 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]
}
}Użycie wzorca 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 stosuje ten sam wzorzec w nowym frameworku Observation, dostępnym od Swift 5.9.
Debugowanie i analizowanie makr
Xcode oferuje kilka narzędzi do debugowania makr i zrozumienia generowanego kodu.
Ekspansja w 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 debuggingLogowanie podczas tworzenia
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. Jest niezbędne, aby zrozumieć, jak poruszać się po węzłach AST podczas implementowania makra.
Dobre praktyki Swift Macros
Tworzenie łatwych w utrzymaniu makr wymaga przestrzegania kilku zasad i unikania typowych pułapek.
Walidacja i komunikaty błędów
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
// ❌ 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
}
"""Generowany kod powinien być równie czytelny jak kod pisany ręcznie, ponieważ deweloperzy będą go analizować przez „Expand Macro".
Podsumowanie
Swift Macros są potężnym narzędziem do eliminowania boilerplate'u przy zachowaniu statycznej kontroli typów. Technologia ta umożliwia:
Najważniejsze punkty:
- ✅ Dwie kategorie: freestanding (
#) i attached (@) - ✅ Pięć ról attached: peer, accessor, member, memberAttribute, conformance
- ✅ Implementację za pomocą swift-syntax i manipulacji AST
- ✅ Obowiązkowe testy z
SwiftSyntaxMacrosTestSupport - ✅ 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 oraz modernizacji API opartych na callbackach w kierunku async/await.
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Tagi
Udostępnij
Powiązane artykuły

Combine vs async/await w Swift: Wzorce Progresywnej Migracji
Kompletny przewodnik po migracji z Combine do async/await w Swift: progresywne strategie, wzorce mostkowania i współistnienie paradygmatów w bazach kodu iOS.

Pytania rekrutacyjne o dostępność iOS w 2026: VoiceOver i Dynamic Type
Przygotuj się do rozmów iOS z kluczowymi pytaniami o dostępność: VoiceOver, Dynamic Type, semantyczne traits i audyty.

Rozmowa Kwalifikacyjna StoreKit 2: Zarządzanie Subskrypcjami i Walidacja Paragonów
Opanuj pytania na rozmowy iOS dotyczące StoreKit 2, zarządzania subskrypcjami, walidacji paragonów i implementacji zakupów w aplikacji z praktycznymi przykładami kodu Swift.