Swift Macros הם תוסף לקומפיילר שמייצר קוד Swift בזמן הקומפילציה על בסיס ניתוח של ה-AST (Abstract Syntax Tree) של הקוד שלך, וזו הדרך הרשמית של Apple להחליף תבניות boilerplate חוזרות במקרו אחד נקי, בטוח-טיפוסים, וניתן לבדיקה. במדריך הזה נכתוב מקרו freestanding ראשון, נעבור למקרו attached שמייצר מאתחל אוטומטית, נבדוק אותו עם assertMacroExpansion ב-Swift Testing, ונראה איך לאתר שגיאות הרחבה ב-Xcode 26.
Swift Macros נתמכים החל מ-Swift 5.9 וזמינים בכל פלטפורמת Apple עם Xcode 15 ומעלה. Xcode 26 הוסיף את #Playground כמקרו מובנה.
קיימים שני סוגי מקרו עיקריים: freestanding (מופעלים עם #name) ו-attached (מופעלים עם @name לפני declaration).
כל מקרו מורכב משני יעדים ב-SPM: מודול ציבורי שמכריז על המקרו ותוסף Compiler Plugin שמכיל את הלוגיקה ותלוי ב-swift-syntax.
בדיקות הרחבה (expansion tests) הן הדרך העיקרית לוודא נכונות. assertMacroExpansion משווה את ה-string לפני ההרחבה לזה שלאחריה.
שגיאות בזמן הרחבה מדווחות דרך context.diagnose, מופיעות בעורך הקוד כמו שגיאות קומפיילר רגילות ויכולות לכלול Fix-it.
השימושים הנפוצים ביותר הם הסרת boilerplate ב-models, יצירת URL בטוחים בזמן קומפילציה, וייצור automatic Codable/Equatable כשהקומפיילר לא מספיק.
מה הם Swift Macros ולמה זה משנה ב-2026
בכנות? אחרי שלוש שנים שבהן השתמשתי במקרו בייצור (מאז שהם נכנסו ב-Swift 5.9), אני יכול להעיד שהמערכת בשלה. השגיאות בזמן הרחבה ב-Xcode 26 נראות היום כמעט כמו שגיאות קומפיילר רגילות, ה-AST יציב, וה-SwiftSyntax מקבל עדכון מסונכרן עם כל גרסת Swift. גם מי שעבד בעבר עם #define ב-Objective-C ימצא כאן עולם אחר: אין החלפת טקסט עיוורת, יש ניתוח עץ תחבירי, ואת תוצאת ההרחבה אפשר לראות בלחיצה ימנית על המקרו והבחירה ב-Expand Macro.
מאחורי הקלעים, מקרו הוא compiler plugin: תוכנית קצרה שרצה כתהליך נפרד תחת swiftc, מקבלת את ה-syntax node של אתר הקריאה, ומחזירה SyntaxNode אחר שהקומפיילר משבץ במקום המקרו. ההפרדה הזו חשובה. כל מקרו הוא קופסה אטומה שלא יכולה לקרוא לפונקציות של האפליקציה שלך או לקבל גישה לזמן ריצה. זו גם הסיבה ש-Swift Macros בטוחים יותר ממקרו של C, הם רואים תחביר, לא טקסט.
החל מ-Xcode 26, Apple מספקת מקרו מובנה חדש בשם #Playground שמאפשר להריץ קטעי קוד בקנבס בלי לצאת מהקובץ. זה רמז ברור לכיוון: Apple עצמה משתמשת במקרו בכל מקום (@Observable, @Model של SwiftData, ו-#expect של Swift Testing), וההמלצה היא לאמץ אותם גם בקוד שלכם כשיש boilerplate חוזר.
מה ההבדל בין freestanding ל-attached macros?
שני סוגי המקרו נבדלים גם בתחביר וגם בתפקיד. freestanding macros נראים כמו פונקציה עם # מקדים, לדוגמה #warning("TODO") או #URL("https://example.com"). הם עומדים בפני עצמם ולא מצמידים את עצמם להצהרה אחרת. מנגד, attached macros מתחילים ב-@ ומצמידים את עצמם ל-declaration (type, property או function), ויכולים להרחיב, להוסיף לה members או להחליף את הגדרתה.
במהלך כתיבת מקרו אני בוחר את הסוג לפי שאלה פשוטה: "האם המקרו יוצר משהו חדש (freestanding) או מוסיף משהו ל-declaration שכבר קיים (attached)?" אם המשתמש כותב let url = #URL("..."), ברור שמדובר ב-freestanding expression. אם הוא רוצה @AutoInit struct User { ... }, מדובר ב-attached member. תפקיד נוסף חשוב הוא @attached(extension), שימושי כשרוצים להוסיף קונפורמיות לפרוטוקול לא חודרני, כפי ש-@Observable עושה.
הקמת חבילת Macro חדשה ב-Xcode 26
הדרך המהירה ביותר היא דרך התבנית הרשמית: File → New → Package…, ואז Multiplatform → Other → Swift Macro. Xcode 26 מייצר ארבעה יעדים מוגדרים מראש: יעד ציבורי שמכריז על המקרו, יעד CompilerPlugin שמכיל את ההטמעה, יעד executable שמדגים שימוש, ויעד בדיקות. התלות החשובה ביותר ב-Package.swift היא swift-syntax, הספרייה הרשמית של Apple לעבודה עם ה-AST.
כדאי לסקור את ה-Package.swift שנוצר. שדה ה-platforms צריך להישאר תואם ל-deployment target של האפליקציה שתשתמש במקרו, אבל יעד ה-Plugin עצמו רץ על המאתחל, לא על המכשיר. לכן ההגדרה שמופיעה היא של macOS. הנה הבסיס המינימלי:
שתי נקודות עדינות. ראשית, גרסת swift-syntax חייבת להתאים לגרסת ה-toolchain: 601.x תואם ל-Swift 6.1/Xcode 26. אם תפתחו עם Xcode מבוגר יותר, צריך להוריד את הטווח ל-510.x או דומה. הכלל הוא: major version של swift-syntax = שנת המהדורה של Swift.
כתיבת המקרו הראשון: #stringify
הדוגמה הקלאסית של Apple היא #stringify: מקרו freestanding שמקבל ביטוי ומחזיר tuple של (התוצאה, הקוד שלו כמחרוזת). זה לא רק "Hello World", זה מקרו שמראה איך מתבצעת המרת AST לטקסט בלי קסמים.
בקובץ הציבורי (AutoInitMacro.swift) נכריז על המקרו ועל הסיגנטורה הציבורית שלו:
בתוך יעד ה-Plugin (StringifyMacro.swift) נטמיע את הלוגיקה. ההטמעה היא struct שמתאים ל-ExpressionMacro ומכריזה על פונקציית expansion סטטית:
// StringifyMacro.swift, plugin implementation
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxMacros
public struct StringifyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.arguments.first?.expression else {
fatalError("compiler bug: missing argument")
}
return "(\(argument), \(literal: argument.description))"
}
}
@main
struct AutoInitMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [StringifyMacro.self]
}
הקריאה #stringify(2 + 3) תורחב בזמן הקומפילציה ל-(2 + 3, "2 + 3"). שימו לב לטריק עם literal:. אופרטור החיבול של string interpolation ב-ExprSyntax מבדיל בין הזרקת AST קיים (\(argument)) לבין הזרקת literal של מחרוזת (\(literal: ...)). אם תשכחו את ה-literal: תקבלו ביטוי בלי גרשיים, וזה לא יקמפל.
מקרו attached: @AutoInit שמייצר מאתחל
עכשיו נכתוב משהו שאני באמת משתמש בו בייצור: מקרו שמייצר מאתחל ציבורי לכל property של struct. תכף תראו למה זה שווה, בעיקר במודולים פנימיים שב-Swift מחייבים מאתחל מפורש כשהמודול חיצוני. במשך שנים פתרתי את זה ידנית עם generate initializer של Xcode, וכל שינוי ב-properties דרש לחזור ולעדכן. עם מקרו זה נעלם.
ההכרזה הציבורית (תפקיד member כי המקרו מוסיף init ל-type):
ההטמעה ב-Plugin מפעילה את SwiftSyntax כדי לסרוק את ה-stored properties של ה-struct:
public struct AutoInitMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let structDecl = declaration.as(StructDeclSyntax.self) else {
throw MacroError.notAStruct
}
let properties: [(name: String, type: String)] =
structDecl.memberBlock.members.compactMap { member in
guard
let variable = member.decl.as(VariableDeclSyntax.self),
variable.bindingSpecifier.tokenKind == .keyword(.let)
|| variable.bindingSpecifier.tokenKind == .keyword(.var),
let binding = variable.bindings.first,
let id = binding.pattern.as(IdentifierPatternSyntax.self),
let type = binding.typeAnnotation?.type
else { return nil }
return (id.identifier.text, type.trimmedDescription)
}
let params = properties
.map { "\($0.name): \($0.type)" }
.joined(separator: ", ")
let assigns = properties
.map { " self.\($0.name) = \($0.name)" }
.joined(separator: "\n")
let initDecl: DeclSyntax = """
public init(\(raw: params)) {
\(raw: assigns)
}
"""
return [initDecl]
}
}
enum MacroError: Error, CustomStringConvertible {
case notAStruct
var description: String {
switch self {
case .notAStruct: "@AutoInit חל רק על structs"
}
}
}
כעת השימוש פשוט במידה מרשימה:
@AutoInit
public struct User {
public let id: UUID
public let name: String
public var email: String
}
// יוצר אוטומטית:
// public init(id: UUID, name: String, email: String) { ... }
הנקודה הקריטית בקוד הזה היא names: named(init) בהכרזה. ללא זה, הקומפיילר לא יודע שהמקרו עומד לייצר init, וקריאה ל-User(id:...) לא תקמפל. תמיד הצהירו בדיוק על השמות שאתם מייצרים. אם המקרו שלכם יוצר עוד members מעבר ל-init, הוסיפו אותם לרשימה: names: named(init), named(description).
איך בודקים Swift Macro?
בדיקת מקרו היא בדיקת הרחבה: לוקחים מחרוזת קלט (קוד עם המקרו), מצפים ממחרוזת פלט (קוד אחרי ההרחבה) ומשתמשים ב-assertMacroExpansion מ-SwiftSyntaxMacrosTestSupport כדי לוודא התאמה. השילוב הזה עובד מצוין יחד עם Swift Testing, המסגרת החדשה של Apple, אבל גם ב-XCTest הקלאסי הוא נראה זהה.
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import AutoInitMacroPlugin
final class AutoInitMacroTests: XCTestCase {
let macros: [String: Macro.Type] = ["AutoInit": AutoInitMacro.self]
func test_autoInit_generatesPublicInit() {
assertMacroExpansion(
"""
@AutoInit
public struct User {
public let id: UUID
public var name: String
}
""",
expandedSource: """
public struct User {
public let id: UUID
public var name: String
public init(id: UUID, name: String) {
self.id = id
self.name = name
}
}
""",
macros: macros
)
}
}
שלוש נקודות חשובות שלמדתי מהשטח. ראשית, expandedSource חייב להתאים בדיוק לפלט, כולל רווחים. ההמלצה היא לכשול בדיקה ראשונה במכוון, להעתיק את הפלט בפועל מההודעה, ולהדביק חזרה. שנית, אם המקרו פולט שגיאה דרך context.diagnose, אפשר לוודא אותה עם הפרמטר diagnostics:. שלישית, אסור לבדוק רק "happy path", צריך לפחות בדיקה אחת לכל סוג שגיאה שהמקרו יודע לפלוט.
איך מאתרים שגיאות בזמן הרחבה?
השגיאה הראשונה שכולם פוגשים: "external macro implementation type not found". זה כמעט תמיד אומר אי-התאמה בין שם המודול ב-#externalMacro(module:) לבין שם יעד ה-.macro ב-Package.swift. בדקו שם, נקודה. הסיבה ב-Xcode 26 דווקא ידידותית: שגיאות הרחבה מוצגות בעורך הקוד עם הקשר ההרחבה, ולחיצה ימנית על המקרו מציעה Expand Macro שמראה בדיוק מה התקבל.
שגיאות שמטרתן להחזיר משוב למשתמש (לא קריסה) צריכות להיפלט כ-Diagnostic דרך ה-context. זה מה שהופך את ההודעה ל"שגיאת קומפיילר" אמיתית עם קו אדום בקובץ של המשתמש:
מעבר לזה, אני ממליץ בחום על שלוש הרגלים. ראשית, להפעיל את הפלאג ב-Xcode עם -emit-macro-expansions כדי לראות את כל ההרחבות שמתבצעות בבנייה. שנית, כשרוצים לאתר לוגיקה מורכבת, להעתיק את ה-expansion לפונקציית עזר נקייה ולכתוב לה unit test בודד. שלישית, להשתמש ב-Swift AST Explorer כדי לוודא ש-structDecl.memberBlock.members מחזיר את מה שאתם חושבים שהוא מחזיר. לא פעם computed property או @Observable מבלבלים את הסריקה.
שימושים נפוצים ושיטות עבודה מומלצות
אחרי שלוש שנים בייצור, אלה השימושים שמצדיקים את ההשקעה:
אתחול מ-Codable: יצירת init(from decoder:) מותאם כש-JSONDecoder ברירת מחדל לא מספיק (כינויי מפתחות, ערכי ברירת מחדל).
URL מאומת בזמן קומפילציה: #URL("https://...") שכושל בבנייה אם המחרוזת לא תקינה, וחוסך ! בכל מקום בקוד.
אובייקטים נצפים בפרוטוקול הישן: יצירת ObjectWillChangePublisher אוטומטי לפני המעבר ל-@Observable, מתאים לפרויקטים שמתקשרים עם בידוד actor ב-Swift 6.2.
ייצור Mock לפרוטוקול: מקרו attached על protocol שמייצר type נלווה MockServiceClient עם closures ניתנים להזרקה.
Validators: לבדיקה של regex/format כבר בזמן הכתיבה.
אבל יש גם אנטי-תבניות שראיתי. אל תשתמשו במקרו רק כדי "לצמצם שורה". מקרו שמייצר print מותאם הוא pure overhead, קוד שאף אחד לא יכול לקרוא בלי להריץ Expand Macro. הכלל הברור שלי: אם הקוד שמייצר את המקרו לא משנה את הסמנטיקה ולא חוסך עשרות שורות חוזרות, אל תכתוב מקרו. מקרו הוא כלי לקירוב boilerplate, לא מסך עשן.
בנוסף, תיעוד Swift הרשמי מדגיש שכדאי לשמור על המקרו טהור: אותה קלט שווה אותה פלט. אסור לקרוא לקבצים, לרשת, או למשתני סביבה. הסיבה היא ש-Xcode מטמין את התוצאה של ההרחבה כדי לזרז את הבנייה, וכל side effect ישבור את ההטמנה.
שאלות נפוצות
מה ההבדל בין Swift Macro לפונקציה רגילה?
פונקציה רצה בזמן ריצה ומחזירה ערך, ואילו מקרו רץ בזמן הקומפילציה ומחזיר קוד שמשובץ במקור. לכן מקרו יכול לייצר declarations חדשות (init, properties, types), מה שפונקציה לא מסוגלת. המחיר: מקרו לא יכול לגשת לזמן ריצה או לקרוא קבצים.
האם Swift Macros נתמכים ב-iOS 17 ומטה?
כן, חלקית. הקוד שהמקרו מייצר חייב להיות תואם ל-deployment target שלכם, אבל המקרו עצמו רץ במאתחל ולכן אינו תלוי בגרסת iOS. שימו לב שמקרו שמייצר API של iOS 17+ ידרוש @available מתאים על הפלט.
למה אני מקבל "external macro implementation type not found"?
הסיבה השכיחה ביותר היא חוסר התאמה בין שם המודול ב-#externalMacro(module: "...", type: "...") לבין שם יעד ה-.macro ב-Package.swift. ודאו ששני השמות זהים תווים-לתווים, ושסוג ה-struct מסומן כ-public.
איך אני רואה את הקוד שהמקרו מייצר?
ב-Xcode 26 לחצו לחיצה ימנית על שם המקרו ובחרו Expand Macro. מסך זמני נפתח עם הקוד שמשובץ בזמן הקומפילציה. לחלופין, אפשר להוסיף את הדגל -Xfrontend -dump-macro-expansions ל-Build Settings כדי לקבל לוג של כל ההרחבות.
האם מקרו יכול לפגוע בזמן הקומפילציה?
כן, אם משתמשים בו בפזיזות. כל מקרו attached מפעיל את ה-Compiler Plugin כתהליך נפרד עבור כל הפעלה. בדרך כלל ההשפעה זניחה (פחות מאחוז בבנייה גדולה), אבל מקרו שמופעל אלפי פעמים יכול להוסיף שניות. הימנעו מקריאת קבצים בתוך expansion ושמרו על האלגוריתם פשוט.
מדריך מקיף ל-Swift Testing, מסגרת הבדיקות החדשה של Apple. תחביר מבוסס מאקרו, בדיקות פרמטריות, מיגרציה הדרגתית מ-XCTest ועבודה עם Swift Concurrency ב-Xcode 26.
Swift 6.2 משנה את כללי המשחק עם בידוד אקטור ברירת מחדל ל-@MainActor. מדריך מעשי שמראה איך להפעיל את הפיצ'ר, להעביר פרויקטים קיימים ולפתור שגיאות פרוטוקול נפוצות — עם דוגמאות קוד מלאות.
איך להשתמש ב-Foundation Models framework של Apple ב-iOS 26 לבניית תכונות בינה מלאכותית על המכשיר עם Swift — כולל Guided Generation, Tool Calling, סטרימינג ב-SwiftUI ודוגמאות קוד מעשיות.