Tüm Yazılar
KategoriSwift
Okuma Süresi
26 dk
Yayın Tarihi
...
Kelime Sayısı
1.394kelime

Kahveni hazırla - bu içerikli bir makale!

Swift 5.9+ Macros ile compile-time code generation. Freestanding ve attached macro türleri, SwiftSyntax ile custom macro yazımı, testing ve production best practices.

Swift Macros Deep Dive: Compile-Time Code Generation Mastery

Swift 5.9 ile tanıtılan Macros, Swift'in en güçlü ve en az anlaşılan özelliklerinden biridir. Boilerplate kodu elimine etmek, compile-time güvenliği artırmak ve kod üretimini otomatikleştirmek için devrim niteliğinde bir araçtır. Bu rehberde macro sistemini A'dan Z'ye öğreneceksiniz.


İçindekiler


Macro Türleri

Swift'te iki ana macro türü vardır:

1. Freestanding Macros

Bağımsız olarak çağrılabilen macro'lar:

swift
1// #stringify - Expression'ı string'e çevirir
2let (value, code) = #stringify(2 + 3)
3// value = 5, code = "2 + 3"
4 
5// #warning ve #error - Compile-time mesajlar
6#warning("Bu kod refactor edilmeli")
7#error("Bu platform desteklenmiyor")
8 
9// Custom URL macro
10let url = #URL("https://api.example.com/users")
11// Compile-time URL validation!

2. Attached Macros

Bir declaration'a bağlanan macro'lar:

swift
1// @Observable - SwiftUI için state management
2@Observable
3class UserViewModel {
4 var name: String = ""
5 var email: String = ""
6}
7// Otomatik olarak ObservableObject uyumluluğu ekler
8 
9// @Model - SwiftData için persistence
10@Model
11class Product {
12 var name: String
13 var price: Decimal
14 var stock: Int
15}

Custom Macro Oluşturma

Proje Yapısı

swift
1MyMacros/
2├── Package.swift
3├── Sources/
4│ ├── MyMacros/ # Macro declarations
5│ │ └── MyMacros.swift
6│ ├── MyMacrosImpl/ # Macro implementations
7│ │ └── MyMacrosImpl.swift
8│ └── MyMacrosClient/ # Test client
9│ └── main.swift
10└── Tests/
11 └── MyMacrosTests/

Package.swift

swift
1// swift-tools-version: 5.9
2import PackageDescription
3import CompilerPluginSupport
4 
5let package = Package(
6 name: "MyMacros",
7 platforms: [.macOS(.v10_15), .iOS(.v13)],
8 products: [
9 .library(name: "MyMacros", targets: ["MyMacros"]),
10 ],
11 dependencies: [
12 .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
13 ],
14 targets: [
15 // Macro implementation
16 .macro(
17 name: "MyMacrosImpl",
18 dependencies: [
19 .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
20 .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
21 ]
22 ),
23 // Library that exposes macros
24 .target(
25 name: "MyMacros",
26 dependencies: ["MyMacrosImpl"]
27 ),
28 // Tests
29 .testTarget(
30 name: "MyMacrosTests",
31 dependencies: [
32 "MyMacrosImpl",
33 .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
34 ]
35 ),
36 ]
37)

@AutoInit Macro

Otomatik initializer oluşturan bir macro yazalım:

swift
1// MyMacros.swift - Macro declaration
2@attached(member, names: named(init))
3public macro AutoInit() = #externalMacro(module: "MyMacrosImpl", type: "AutoInitMacro")
4 
5// MyMacrosImpl.swift - Macro implementation
6import SwiftSyntax
7import SwiftSyntaxMacros
8import SwiftCompilerPlugin
9 
10public struct AutoInitMacro: MemberMacro {
11 public static func expansion(
12 of node: AttributeSyntax,
13 providingMembersOf declaration: some DeclGroupSyntax,
14 in context: some MacroExpansionContext
15 ) throws -> [DeclSyntax] {
16
17 // Get stored properties
18 let properties = declaration.memberBlock.members
19 .compactMap { $0.decl.as(VariableDeclSyntax.self) }
20 .filter { $0.bindingSpecifier.tokenKind == .keyword(.var) }
21 .flatMap { $0.bindings }
22 .compactMap { binding -> (String, String)? in
23 guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self),
24 let type = binding.typeAnnotation?.type else {
25 return nil
26 }
27 return (pattern.identifier.text, type.description.trimmingCharacters(in: .whitespaces))
28 }
29
30 // Generate initializer
31 let parameters = properties
32 .map { "\($0.0): \($0.1)" }
33 .joined(separator: ", ")
34
35 let assignments = properties
36 .map { "self.\($0.0) = \($0.0)" }
37 .joined(separator: "\n ")
38
39 let initDecl: DeclSyntax = """
40 init(\(raw: parameters)) {
41 \(raw: assignments)
42 }
43 """
44
45 return [initDecl]
46 }
47}
48 
49@main
50struct MyMacrosPlugin: CompilerPlugin {
51 let providingMacros: [Macro.Type] = [
52 AutoInitMacro.self,
53 ]
54}

Kullanım

swift
1import MyMacros
2 
3@AutoInit
4struct User {
5 var id: UUID
6 var name: String
7 var email: String
8 var age: Int
9}
10 
11// Generates:
12// init(id: UUID, name: String, email: String, age: Int) {
13// self.id = id
14// self.name = name
15// self.email = email
16// self.age = age
17// }
18 
19let user = User(id: UUID(), name: "John", email: "[email protected]", age: 30)

@Codable Enhancement Macro

swift
1@attached(member, names: named(CodingKeys), named(init(from:)), named(encode(to:)))
2@attached(extension, conformances: Codable)
3public macro EnhancedCodable() = #externalMacro(module: "MyMacrosImpl", type: "EnhancedCodableMacro")
4 
5// Usage
6@EnhancedCodable
7struct Product {
8 @CodableKey("product_name")
9 var name: String
10
11 @CodableKey("unit_price")
12 var price: Decimal
13
14 @CodableIgnore
15 var localCache: Data?
16}

#URL Macro - Compile-Time Validation

swift
1@freestanding(expression)
2public macro URL(_ string: String) -> URL = #externalMacro(module: "MyMacrosImpl", type: "URLMacro")
3 
4public struct URLMacro: ExpressionMacro {
5 public static func expansion(
6 of node: some FreestandingMacroExpansionSyntax,
7 in context: some MacroExpansionContext
8 ) throws -> ExprSyntax {
9 guard let argument = node.argumentList.first?.expression,
10 let stringLiteral = argument.as(StringLiteralExprSyntax.self),
11 let urlString = stringLiteral.segments.first?.as(StringSegmentSyntax.self)?.content.text else {
12 throw MacroError.invalidArgument
13 }
14
15 // Compile-time validation
16 guard URL(string: urlString) != nil else {
17 throw MacroError.invalidURL(urlString)
18 }
19
20 return "URL(string: \(argument))!"
21 }
22}
23 
24// Usage
25let apiURL = #URL("https://api.example.com") // ✅ Compiles
26let badURL = #URL("not a url") // ❌ Compile error!

Testing Macros

swift
1import SwiftSyntaxMacrosTestSupport
2import XCTest
3 
4final class AutoInitMacroTests: XCTestCase {
5 func testAutoInit() {
6 assertMacroExpansion(
7 """
8 @AutoInit
9 struct User {
10 var name: String
11 var age: Int
12 }
13 """,
14 expandedSource: """
15 struct User {
16 var name: String
17 var age: Int
18 init(name: String, age: Int) {
19 self.name = name
20 self.age = age
21 }
22 }
23 """,
24 macros: ["AutoInit": AutoInitMacro.self]
25 )
26 }
27}

Macro Error Handling

swift
1// MARK: - Custom Macro Errors
2enum MacroError: Error, DiagnosticMessage {
3 case invalidApplication(String)
4 case missingArgument(String)
5 case invalidURL(String)
6
7 var message: String {
8 switch self {
9 case .invalidApplication(let msg): return msg
10 case .missingArgument(let arg): return "Missing argument: \(arg)"
11 case .invalidURL(let url): return "Invalid URL: \(url)"
12 }
13 }
14
15 var diagnosticID: MessageID {
16 MessageID(domain: "MyMacros", id: String(describing: self))
17 }
18
19 var severity: DiagnosticSeverity { .error }
20}

Macro Testing

swift
1import SwiftSyntaxMacrosTestSupport
2import XCTest
3 
4final class MyMacrosTests: XCTestCase {
5 let testMacros: [String: Macro.Type] = [
6 "AutoInit": AutoInitMacro.self,
7 "URL": URLMacro.self,
8 ]
9
10 func testAutoInit_SimpleStruct() throws {
11 assertMacroExpansion(
12 """
13 @AutoInit
14 struct User {
15 var name: String
16 var age: Int
17 }
18 """,
19 expandedSource: """
20 struct User {
21 var name: String
22 var age: Int
23
24 internal init(name: String, age: Int) {
25 self.name = name
26 self.age = age
27 }
28 }
29 """,
30 macros: testMacros
31 )
32 }
33}

Best Practices

  1. Macro'ları basit tutun: Tek bir işi iyi yapın
  2. Hata mesajları anlaşılır olsun: Kullanıcıya ne yanlış olduğunu açıklayın
  3. Test yazın: Her macro için kapsamlı testler
  4. Documentation: Macro'nun ne ürettiğini belgeleyin
  5. Performans: Compile-time'ı uzatmamaya dikkat edin

Macro Türleri Karşılaştırması

Macro Rolü
Anahtar Kelime
Açıklama
Kullanım Alanı
Expression
@freestanding(expression)
Değer döndürür
#URL, #stringify
Declaration
@freestanding(declaration)
Yeni declaration oluşturur
#warning, #error
Peer
@attached(peer)
Yanına declaration ekler
Async wrapper
Member
@attached(member)
Tipe member ekler
@AutoInit
Accessor
@attached(accessor)
get/set/didSet ekler
@Published benzeri
MemberAttribute
@attached(memberAttribute)
Member'lara attribute ekler
@CodableKey
Extension
@attached(extension)
Extension + conformance ekler
@Observable
💡 Altın İpucu: Macro geliştirirken assertMacroExpansion test fonksiyonunu her zaman kullanın. Macro'nuzun ürettiği kodu string olarak doğrulamak, beklenmeyen kod üretimini compile-time yerine test aşamasında yakalamanızı sağlar. Ayrıca Xcode'da herhangi bir macro kullanımına sağ tıklayıp "Expand Macro" seçeneği ile üretilen kodu anında görebilirsiniz.

Okuyucu Ödülü

Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:

ALTIN İPUCU

Bu yazının en değerli bilgisi

Bu ipucu, yazının en önemli çıkarımını içeriyor.

Sonuç

Swift Macros, boilerplate kodu elimine etmek ve compile-time güvenliği artırmak için güçlü bir araçtır. Custom macro'lar yazarak kod tabanınızı daha temiz ve güvenli hale getirebilirsiniz.

Kaynaklar

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?

Etiketler

#Swift#Macros#Swift 5.9#Code Generation#Metaprogramming
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz