[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
---
|
||||
id: ios-swift-macros
|
||||
title: Swift Macros — Compile-time Code Generation
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [ios, swift, macro, vibe-coding]
|
||||
tech_stack: { language: "Swift 5.9+", applicable_to: ["iOS"] }
|
||||
applied_in: []
|
||||
aliases: [Swift Macros, @attached, @freestanding, SwiftSyntax, Codable derivation]
|
||||
---
|
||||
|
||||
# Swift Macros (Swift 5.9+)
|
||||
|
||||
> Compile-time 코드 생성. **Boilerplate 제거 (Codable, Observation, Mocking)**. SwiftSyntax 로 작성. 옛 propertyWrapper / @objc dynamic 보다 강력.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- @freestanding: `#myMacro(...)` — expression / declaration 자체.
|
||||
- @attached: `@MyMacro` — 다른 declaration 에 붙음.
|
||||
- 종류: member, peer, accessor, conformance, extension.
|
||||
- SwiftSyntax: AST 변형.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### 사용 (built-in)
|
||||
```swift
|
||||
// @Observable (iOS 17+, Observation framework)
|
||||
@Observable
|
||||
class ViewModel {
|
||||
var name = ""
|
||||
var items: [Item] = []
|
||||
}
|
||||
|
||||
// 매크로 expand:
|
||||
class ViewModel {
|
||||
private let _$observationRegistrar = ObservationRegistrar()
|
||||
|
||||
@ObservationTracked var name = ""
|
||||
@ObservationTracked var items: [Item] = []
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
// SwiftData
|
||||
@Model
|
||||
final class Note {
|
||||
var title: String
|
||||
var body: String
|
||||
var createdAt: Date
|
||||
|
||||
init(title: String, body: String) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
// SwiftUI Preview
|
||||
#Preview("Light") {
|
||||
ContentView()
|
||||
}
|
||||
|
||||
#Preview("Dark") {
|
||||
ContentView()
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
// Stringify (test)
|
||||
let result = #stringify(1 + 2)
|
||||
// → ("1 + 2", 3)
|
||||
```
|
||||
|
||||
### 자체 macro 작성 — package 구조
|
||||
```
|
||||
MyMacros/
|
||||
├── Package.swift
|
||||
├── Sources/
|
||||
│ ├── MyMacros/ (consumer 가 import)
|
||||
│ │ └── MyMacros.swift (declarations)
|
||||
│ └── MyMacrosImpl/ (compiler plugin)
|
||||
│ ├── MyMacrosPlugin.swift
|
||||
│ └── ConstantMacro.swift
|
||||
└── Tests/
|
||||
```
|
||||
|
||||
```swift
|
||||
// Package.swift
|
||||
import PackageDescription
|
||||
import CompilerPluginSupport
|
||||
|
||||
let package = Package(
|
||||
name: "MyMacros",
|
||||
platforms: [.iOS(.v17), .macOS(.v14)],
|
||||
products: [.library(name: "MyMacros", targets: ["MyMacros"])],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.macro(name: "MyMacrosImpl", dependencies: [
|
||||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
||||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
|
||||
]),
|
||||
.target(name: "MyMacros", dependencies: ["MyMacrosImpl"]),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Freestanding expression macro
|
||||
```swift
|
||||
// MyMacros.swift (declaration)
|
||||
@freestanding(expression)
|
||||
public macro stringify<T>(_ value: T) -> (String, T) = #externalMacro(
|
||||
module: "MyMacrosImpl", type: "StringifyMacro"
|
||||
)
|
||||
|
||||
// MyMacrosImpl/StringifyMacro.swift
|
||||
import SwiftSyntax
|
||||
import SwiftSyntaxMacros
|
||||
|
||||
public struct StringifyMacro: ExpressionMacro {
|
||||
public static func expansion(
|
||||
of node: some FreestandingMacroExpansionSyntax,
|
||||
in context: some MacroExpansionContext
|
||||
) -> ExprSyntax {
|
||||
guard let arg = node.argumentList.first?.expression else {
|
||||
fatalError("compiler bug")
|
||||
}
|
||||
return "(\(literal: arg.description), \(arg))"
|
||||
}
|
||||
}
|
||||
|
||||
// 사용
|
||||
let r = #stringify(2 + 3) // ("2 + 3", 5)
|
||||
```
|
||||
|
||||
### Attached member macro (struct 에 method 추가)
|
||||
```swift
|
||||
@attached(member, names: arbitrary)
|
||||
public macro CaseDetection() = #externalMacro(...)
|
||||
|
||||
@CaseDetection
|
||||
enum Status {
|
||||
case open
|
||||
case paid
|
||||
case shipped
|
||||
}
|
||||
|
||||
// Expand 후:
|
||||
extension Status {
|
||||
var isOpen: Bool { if case .open = self { return true } else { return false } }
|
||||
var isPaid: Bool { ... }
|
||||
var isShipped: Bool { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin
|
||||
```swift
|
||||
// MyMacrosPlugin.swift
|
||||
import SwiftCompilerPlugin
|
||||
import SwiftSyntaxMacros
|
||||
|
||||
@main
|
||||
struct MyMacrosPlugin: CompilerPlugin {
|
||||
let providingMacros: [Macro.Type] = [
|
||||
StringifyMacro.self,
|
||||
CaseDetectionMacro.self,
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Test
|
||||
```swift
|
||||
import SwiftSyntaxMacrosTestSupport
|
||||
import XCTest
|
||||
|
||||
final class StringifyMacroTests: XCTestCase {
|
||||
func testStringify() {
|
||||
assertMacroExpansion(
|
||||
"#stringify(2 + 3)",
|
||||
expandedSource: """
|
||||
("2 + 3", 2 + 3)
|
||||
""",
|
||||
macros: ["stringify": StringifyMacro.self]
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Diagnostic (에러)
|
||||
```swift
|
||||
public struct MyMacro: MemberMacro {
|
||||
public static func expansion(
|
||||
of node: AttributeSyntax,
|
||||
providingMembersOf decl: some DeclGroupSyntax,
|
||||
in context: some MacroExpansionContext
|
||||
) throws -> [DeclSyntax] {
|
||||
guard decl.is(StructDeclSyntax.self) else {
|
||||
context.diagnose(Diagnostic(
|
||||
node: node,
|
||||
message: SimpleDiagnosticMessage(
|
||||
message: "@MyMacro can only be applied to structs",
|
||||
diagnosticID: MessageID(domain: "MyMacros", id: "structOnly"),
|
||||
severity: .error
|
||||
)
|
||||
))
|
||||
return []
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 실용 예 — Mock 자동 생성
|
||||
```swift
|
||||
@AutoMock
|
||||
protocol UserService {
|
||||
func fetchUser(id: String) async throws -> User
|
||||
func saveUser(_ user: User) async throws
|
||||
}
|
||||
|
||||
// Expand 후
|
||||
class UserServiceMock: UserService {
|
||||
var fetchUserCalls: [String] = []
|
||||
var fetchUserResult: Result<User, Error> = .failure(MockError.notSet)
|
||||
|
||||
func fetchUser(id: String) async throws -> User {
|
||||
fetchUserCalls.append(id)
|
||||
return try fetchUserResult.get()
|
||||
}
|
||||
|
||||
var saveUserCalls: [User] = []
|
||||
func saveUser(_ user: User) async throws {
|
||||
saveUserCalls.append(user)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 사용 | 추천 |
|
||||
|---|---|
|
||||
| Boilerplate 제거 | Macro |
|
||||
| Codable customize | Macro 또는 ExtendingMacro |
|
||||
| Mock 생성 | AutoMock-style macro |
|
||||
| 단순 helper | 일반 함수 / extension 충분 |
|
||||
| Ad-hoc | macro 만들지 말 것 |
|
||||
| Public library | macro 가 powerful |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **모든 거 macro**: 디버깅 어려움. 일반 코드 우선.
|
||||
- **Macro 안 외부 의존 (network)**: deterministic 깨짐. 순수 syntax 변환만.
|
||||
- **Diagnostic 없음**: 사용자 잘못 사용 시 cryptic error.
|
||||
- **Test 없음**: regression 잡기 어려움.
|
||||
- **너무 복잡 generated code**: 디버그 못 함. 단순 전개.
|
||||
- **String concat 으로 syntax**: SwiftSyntax 안전.
|
||||
- **Macro 가 다른 macro 의존**: order 의존.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- @Observable / @Model / SwiftData / Preview = built-in.
|
||||
- 자체 macro = SwiftSyntax 학습 필요.
|
||||
- Diagnostic + test 항상.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[iOS_SwiftUI_State_Property_Wrappers]]
|
||||
- [[iOS_SwiftData_Patterns]]
|
||||
- [[iOS_Swift_Concurrency_Actor_Patterns]]
|
||||
Reference in New Issue
Block a user