Swift Macros: Πλήρης Οδηγός για @freestanding και @attached με Παραδείγματα

Μάθε Swift Macros από το μηδέν: πώς λειτουργούν freestanding και attached macros, πώς δημιουργείς δικά σου, και πραγματικά παραδείγματα όπως @Observable, #Predicate και custom peer macros για Swift 6 και Xcode 16.

Swift Macros 2026: @freestanding & @attached

Ας είμαστε ειλικρινείς: τα Swift Macros ήταν ίσως η πιο ενδιαφέρουσα προσθήκη στη Swift 5.9, και από τότε έχουν εξελιχθεί σε ένα από τα πιο ισχυρά εργαλεία του οικοσυστήματος της Apple. Σου επιτρέπουν να μεταφέρεις επαναλαμβανόμενη λογική στο compile time, μειώνοντας τον boilerplate κώδικα — χωρίς να θυσιάζεις την ασφάλεια τύπων. Σε αυτόν τον αναλυτικό οδηγό για το 2026 θα δούμε λεπτομερώς πώς λειτουργούν τα freestanding και attached macros, πώς φτιάχνεις δικά σου από την αρχή, και κυρίως ποια είναι τα παραδείγματα που θα συναντήσεις πραγματικά σε production iOS εφαρμογές.

Τι είναι τα Swift Macros και Γιατί τα Χρειαζόμαστε

Σε αντίθεση με τα C preprocessor macros (που λειτουργούν με απλή αντικατάσταση συμβολοσειρών, κάτι αρκετά πρωτόγονο αν το σκεφτείς), τα Swift Macros είναι πραγματικός Swift κώδικας που εκτελείται κατά τη μεταγλώττιση. Δέχονται ως είσοδο ένα syntax tree μέσω της βιβλιοθήκης SwiftSyntax και παράγουν νέο Swift κώδικα, ο οποίος ελέγχεται πλήρως από τον compiler για ορθότητα τύπων.

Το πιο κρίσιμο χαρακτηριστικό; Λειτουργούν αθροιστικά. Μπορούν δηλαδή να προσθέσουν νέο κώδικα, αλλά ποτέ δεν διαγράφουν ή τροποποιούν τον υπάρχοντα. Αυτή η αρχή κάνει τη συμπεριφορά τους προβλέψιμη και τον παραγόμενο κώδικα εύκολο να εξεταστεί — κάτι που εγώ προσωπικά εκτιμώ πολύ μετά από χρόνια ταλαιπωρίας με runtime reflection σε άλλες γλώσσες.

Βασικά Πλεονεκτήματα

  • Compile-time εκτέλεση: Καμία επιβάρυνση runtime performance
  • Type-safe μετασχηματισμοί: Πλήρης έλεγχος τύπων στον παραγόμενο κώδικα
  • Μείωση boilerplate: Αυτόματη παραγωγή επαναλαμβανόμενων μοτίβων
  • Sandboxed εκτέλεση: Δεν έχουν πρόσβαση σε filesystem ή δίκτυο (καλό από άποψη ασφάλειας)
  • Διαφάνεια: Μπορείς να δεις τον παραγόμενο κώδικα στο Xcode με δεξί κλικ → Expand Macro

Οι Δύο Κατηγορίες: Freestanding και Attached Macros

Τα Swift Macros χωρίζονται σε δύο μεγάλες κατηγορίες, με αρκετά διαφορετική σύνταξη και σκοπό:

  • Freestanding macros — χρησιμοποιούν το πρόθεμα # και στέκονται μόνα τους ως εκφράσεις ή δηλώσεις
  • Attached macros — χρησιμοποιούν το πρόθεμα @ και προσαρτώνται σε υπάρχουσες δηλώσεις (τύπους, μεταβλητές, συναρτήσεις)

Freestanding Macros σε Βάθος

Τα freestanding macros υπάρχουν σε δύο ρόλους: το @freestanding(expression) για macros που επιστρέφουν τιμή, και το @freestanding(declaration) για macros που παράγουν νέες δηλώσεις. Απλό μέχρι εδώ.

Παράδειγμα: Το #stringify Macro

Αυτό είναι το προεπιλεγμένο παράδειγμα που δημιουργεί το Xcode όταν φτιάχνεις νέο Swift Macro package — μάλλον το πρώτο που έχεις δει κι εσύ. Δέχεται μια έκφραση και επιστρέφει ένα tuple με την τιμή της και τον πηγαίο κώδικα ως string:

@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) =
    #externalMacro(module: "MyMacrosImpl", type: "StringifyMacro")

// Χρήση
let (result, source) = #stringify(2 + 3)
print(result)  // 5
print(source)  // "2 + 3"

Παράδειγμα: Compile-Time URL Validation

Ένα από τα πιο πρακτικά freestanding macros είναι το #URL. Επικυρώνει τη συμβολοσειρά κατά τη μεταγλώττιση και προκαλεί compile error αν είναι μη έγκυρη — αντικαθιστώντας την επικίνδυνη force-unwrap πρακτική που, ομολογουμένως, όλοι μας έχουμε γράψει κάποια στιγμή:

// Χωρίς macro — δυνητικό crash στο runtime
let url = URL(string: "https://api.example.com/v1")!

// Με macro — compile-time validation
let safeUrl = #URL("https://api.example.com/v1")
// Αν γράψεις #URL("not a url") παίρνεις compile error

Built-in Freestanding Macros που Πρέπει να Γνωρίζεις

// #Predicate — δομημένα queries για SwiftData / Core Data
let adults = #Predicate<Person> { person in
    person.age >= 18 && person.isActive
}

// #warning — προσαρμοσμένα compile-time warnings
#warning("Αυτή η API θα καταργηθεί στη v3.0 — μεταναστεύστε στο NewService")

// #file, #line, #function — source location macros (πάντα διαθέσιμα)
func logError(_ message: String, file: String = #file, line: Int = #line) {
    print("[\(file):\(line)] \(message)")
}

Attached Macros σε Βάθος

Εδώ τα πράγματα γίνονται πιο ενδιαφέροντα. Τα attached macros προσαρτώνται σε υπάρχουσες δηλώσεις και μπορούν να παίξουν έναν ή περισσότερους από τους ακόλουθους ρόλους:

  • @attached(peer) — προσθέτει νέες δηλώσεις δίπλα στην προσαρτημένη
  • @attached(accessor) — προσθέτει getter/setter accessors σε ιδιότητες
  • @attached(memberAttribute) — προσθέτει attributes στα μέλη ενός τύπου
  • @attached(member) — προσθέτει νέα μέλη μέσα σε τύπο ή extension
  • @attached(extension) — προσθέτει extensions στον τύπο (αντικαθιστά το παλιό conformance)

Παράδειγμα: Το @Observable Macro

Πιθανότατα το έχεις ήδη χρησιμοποιήσει στη SwiftUI. Το @Observable αντικαθιστά το παλιό ObservableObject protocol με @Published properties, μειώνοντας δραστικά τον boilerplate. Δες τη διαφορά:

// Πριν τα macros
class UserViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var email: String = ""
    @Published var isLoading: Bool = false
}

// Με το @Observable macro
@Observable
class UserViewModel {
    var name: String = ""
    var email: String = ""
    var isLoading: Bool = false
}

Στο SwiftUI view, χρησιμοποιείς απλά @State αντί για @StateObject ή @ObservedObject — πιο καθαρό, πιο intuitive:

struct ProfileView: View {
    @State private var viewModel = UserViewModel()

    var body: some View {
        Form {
            TextField("Όνομα", text: $viewModel.name)
            TextField("Email", text: $viewModel.email)
        }
    }
}

Παράδειγμα: Custom Peer Macro για Async APIs

Να ένα peer macro που μετατρέπει αυτόματα completion-based functions σε async/await versions — τύπου χρυσός αν έχεις μεγάλο legacy codebase:

@attached(peer, names: overloaded)
public macro AddAsync() =
    #externalMacro(module: "MyMacrosImpl", type: "AddAsyncMacro")

// Χρήση
@AddAsync
func fetchUser(id: String, completion: @escaping (User?) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, _ in
        completion(data.flatMap { try? JSONDecoder().decode(User.self, from: $0) })
    }.resume()
}

// Το macro παράγει αυτόματα την async έκδοση
func fetchUser(id: String) async -> User? {
    await withCheckedContinuation { continuation in
        fetchUser(id: id) { user in
            continuation.resume(returning: user)
        }
    }
}

Δημιουργία Custom Macro Βήμα προς Βήμα

Ώρα να βρωμίσουμε τα χέρια μας. Για να φτιάξεις δικό σου macro από το Xcode:

  1. Επίλεξε File → New → Package...
  2. Διάλεξε το template Swift Macro
  3. Δώσε όνομα στο package (π.χ. MyMacros)

Δομή του Macro Package

Το αυτόματα παραγόμενο package περιέχει τα εξής αρχεία:

  • MyMacros.swift — δηλώσεις των macros (το public API)
  • MyMacrosMacro.swift — η υλοποίηση χρησιμοποιώντας SwiftSyntax
  • MyMacrosClient/main.swift — εκτελέσιμο για δοκιμή
  • Tests/MyMacrosTests/MyMacrosTests.swift — unit tests

Υλοποίηση ενός @CaseDetection Macro

Ας φτιάξουμε ένα member macro που, για κάθε case ενός enum, παράγει μια computed property τύπου Bool — έλεγχος δηλαδή αν είναι η τρέχουσα τιμή. Πολύ συνηθισμένο pattern όταν δουλεύεις με state machines:

import SwiftSyntax
import SwiftSyntaxMacros

public struct CaseDetectionMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
            return []
        }

        return enumDecl.memberBlock.members
            .compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
            .flatMap { $0.elements }
            .map { element in
                let name = element.name.text
                return DeclSyntax(stringLiteral: """
                    var is\(name.prefix(1).uppercased() + name.dropFirst()): Bool {
                        if case .\(name) = self { return true }
                        return false
                    }
                    """)
            }
    }
}

Η δήλωση του macro στο public module:

@attached(member, names: arbitrary)
public macro CaseDetection() =
    #externalMacro(module: "MyMacrosImpl", type: "CaseDetectionMacro")

// Παράδειγμα χρήσης
@CaseDetection
enum NetworkState {
    case idle
    case loading
    case loaded(data: Data)
    case failed(error: Error)
}

// Παράγονται αυτόματα:
// var isIdle: Bool { ... }
// var isLoading: Bool { ... }
// var isLoaded: Bool { ... }
// var isFailed: Bool { ... }

Testing για Swift Macros

Η Apple παρέχει το framework SwiftSyntaxMacrosTestSupport για unit testing των macros, με τη συνάρτηση assertMacroExpansion. Και ναι, πρέπει οπωσδήποτε να γράφεις tests — τα macros χωρίς tests είναι δράκοι σε αναμονή:

import XCTest
import SwiftSyntaxMacrosTestSupport
@testable import MyMacrosImpl

final class CaseDetectionTests: XCTestCase {
    func testCaseDetectionExpansion() {
        assertMacroExpansion(
            """
            @CaseDetection
            enum Status {
                case active
                case inactive
            }
            """,
            expandedSource: """
            enum Status {
                case active
                case inactive

                var isActive: Bool {
                    if case .active = self { return true }
                    return false
                }
                var isInactive: Bool {
                    if case .inactive = self { return true }
                    return false
                }
            }
            """,
            macros: ["CaseDetection": CaseDetectionMacro.self]
        )
    }
}

Βέλτιστες Πρακτικές για Swift Macros

1. Κράτα τα Macros Απλά

Όσο πιο πολύπλοκο γίνεται ένα macro, τόσο πιο δύσκολο είναι να γίνει debug. Αν χρειάζεσαι σύνθετη λογική, χώρισέ την σε πολλά μικρότερα macros. Πίστεψέ με σε αυτό — έχω χάσει ώρες προσπαθώντας να debug-άρω ένα macro που έκανε τρία πράγματα ταυτόχρονα.

2. Πάντα Παρέχε Καλά Compile-Time Errors

Χρησιμοποίησε το context.diagnose(...) για να δίνεις σαφή μηνύματα όταν το macro δεν μπορεί να εφαρμοστεί:

guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
    let diagnostic = Diagnostic(
        node: node,
        message: MacroError.notAnEnum
    )
    context.diagnose(diagnostic)
    return []
}

3. Απόφυγε την Υπερβολική Χρήση

Τα macros είναι ισχυρά, αλλά κάνουν τον κώδικα λιγότερο διαφανή. Χρησιμοποίησέ τα όταν αληθινά μειώνουν boilerplate, όχι για να κρύψεις λογική. Το "magic code" σπάνια ενθουσιάζει τον επόμενο που θα το διαβάσει.

4. Έλεγξε τον Παραγόμενο Κώδικα

Στο Xcode, κάνε δεξί κλικ σε ένα macro call και επίλεξε Expand Macro. Είναι το πιο σημαντικό εργαλείο για να καταλάβεις τι ακριβώς συμβαίνει — και ειλικρινά, η πρώτη φορά που το ανακάλυψα ήταν αποκάλυψη.

Συνηθισμένα Σφάλματα και Πώς να τα Αντιμετωπίσεις

"External macro implementation could not be found"

Αυτό συμβαίνει συνήθως όταν το όνομα του module στη δήλωση #externalMacro δεν ταιριάζει με το πραγματικό όνομα του compiler plugin target. Έλεγξε το Package.swift — εκεί κρύβεται σχεδόν πάντα το πρόβλημα.

"Macros are not enabled"

Στο Xcode 16+, την πρώτη φορά που χρησιμοποιείς ένα macro από ένα package, ο compiler ζητά άδεια (για λόγους ασφάλειας). Πάτησε Trust & Enable και είσαι έτοιμος.

Συχνές Ερωτήσεις (FAQ)

Ποια έκδοση του Swift απαιτείται για τα Swift Macros;

Τα Swift Macros απαιτούν Swift 5.9 ή νεότερο και Xcode 15+. Για την πλήρη γκάμα δυνατοτήτων (συμπεριλαμβανομένου του @attached(extension)), προτείνεται Swift 6.0+ με Xcode 16 ή μεταγενέστερο.

Επηρεάζουν τα macros την απόδοση runtime;

Όχι. Τα macros επεκτείνονται κατά τη μεταγλώττιση και ο παραγόμενος κώδικας μεταγλωττίζεται όπως οποιοσδήποτε άλλος Swift κώδικας. Δεν υπάρχει runtime cost. Ωστόσο, μπορεί να αυξήσουν αισθητά τον χρόνο compilation σε μεγάλα projects — κάτι που αξίζει να το ξέρεις.

Ποια είναι η διαφορά μεταξύ macros και code generation εργαλείων όπως το Sourcery;

Σε αντίθεση με τα code generation tools που τρέχουν ως ξεχωριστό βήμα build και παράγουν αρχεία στον δίσκο, τα Swift Macros εκτελούνται μέσα στον compiler και ο κώδικάς τους δεν αποθηκεύεται. Επίσης, είναι πλήρως type-aware και ενσωματωμένα στο IDE.

Μπορώ να χρησιμοποιήσω macros σε iOS app development χωρίς να φτιάξω custom;

Φυσικά. Τα built-in macros της Apple όπως @Observable, @Model (SwiftData), #Predicate, και #Preview καλύπτουν τις περισσότερες ανάγκες ενός σύγχρονου iOS developer — χωρίς να χρειαστεί να γράψεις δικά σου.

Πώς κάνω debug ένα macro που δεν παράγει τον κώδικα που περιμένω;

Η πιο αποτελεσματική προσέγγιση είναι ο συνδυασμός τριών τεχνικών: (1) χρησιμοποίησε το Expand Macro στο Xcode για να δεις τον παραγόμενο κώδικα, (2) γράψε unit tests με assertMacroExpansion για να επαληθεύσεις το expansion, και (3) πρόσθεσε print(syntax.description) μέσα στη συνάρτηση expansion για να δεις το input syntax tree.

Συμπέρασμα

Τα Swift Macros αποτελούν θεμελιώδες εργαλείο για κάθε σύγχρονο iOS developer το 2026. Είτε χρησιμοποιείς τα built-in macros της Apple όπως το @Observable και το @Model, είτε φτιάχνεις δικά σου για να αυτοματοποιήσεις επαναλαμβανόμενα μοτίβα στο codebase σου, η κατανόηση του πώς λειτουργούν θα κάνει τον κώδικά σου πιο καθαρό, ασφαλέστερο και πιο εκφραστικό.

Ξεκίνα με τα built-in, εξοικειώσου με τη χρήση τους, και όταν εντοπίσεις boilerplate που επαναλαμβάνεται σε όλο το project, σκέψου αν ένα custom macro θα μπορούσε να το εξαλείψει. Αυτή είναι η σωστή προσέγγιση για να εκμεταλλευτείς στο έπακρο τη δύναμη του Swift compile-time metaprogramming.

Σχετικά με τον Συγγραφέα Daniel Okafor

Daniel is a former Spotify iOS engineer (2019-2024) who worked on the Now Playing surface and the in-app podcast player. He shipped the SwiftUI rewrite of the lyrics view to over 600 million users and contributed several fixes upstream to swift-collections. His writing tends toward the unglamorous corners of iOS work: build-time regressions in Xcode 16, why SwiftData still isn't ready for production sync scenarios, and how to instrument a real app with os_signpost without drowning in noise. He spent two years before Spotify at a fintech startup in Berlin building a banking app on top of Solaris API. Daniel now freelances out of Lisbon and maintains a small open-source library for type-safe deep links in SwiftUI. He has 9 years of native iOS experience.