# Swift Macros:メタプログラミングの実践例 > Swift Macros の完全ガイドです。freestanding と attached マクロの作成、swift-syntax を用いた AST 操作、ボイラープレートを排除する実践的なコード例をまとめています。 - Published: 2026-03-11 - Updated: 2026-05-01 - Author: SharpSkill - Tags: swift, ios, macros, metaprogramming, swift-syntax - Reading time: 16 min --- Swift 5.9 と Xcode 15 で導入された Swift Macros は、Swift コードの書き方に革新をもたらします。コンパイル時にコードを生成できるようになり、静的型安全性を保ったままボイラープレートを排除できます。C のプリプロセッサマクロとは異なり、Swift Macros は型安全であり、コンパイラに統合され、開発ツールから完全にサポートされています。 > **このガイドの内容** > > 本ガイドでは、Swift Macros の作成を最初から最後まで取り上げます。基礎概念から高度な実装まで網羅し、どの iOS プロジェクトにも応用しやすい動作するコード例を交えて解説します。 ## Swift Macros の種類を理解する Swift にはマクロのカテゴリが大きく 2 つあり、それぞれ異なるユースケースに対応します。freestanding マクロは式や宣言として単独で機能し、attached マクロは既存の宣言に紐づき、それを修正・拡張します。 ### freestanding マクロ:式と宣言 freestanding マクロは `#` 記号で始まり、値を返す(式)か新しい宣言を生成します。次は具体的な式マクロの例です。 ```swift // MacroUsage.swift // 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 pending ``` 式と宣言の根本的な違いは結果にあります。式は値を返し、宣言は構造的なコード(型、関数、変数)を生成します。 ### attached マクロ:5 つのロール attached マクロは `@` で始まり、宣言の前に配置します。Swift はこのマクロに対して 5 つのロールを定義しています。 ```swift // AttachedMacroRoles.swift // @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 { ... } ``` これらのロールを組み合わせることで、コードを複数の側面で同時に変換する強力なマクロを構築できます。 ## マクロを作るためのプロジェクト構成 Swift Macros の作成には、特定の構成を持つ Swift Package が必要です。このパッケージは Swift コードを抽象構文木(AST)として扱う公式ライブラリ swift-syntax に依存します。 ### Package.swift の構造 ```swift // 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") ] ) ] ) ``` この構成では、マクロの宣言(クライアントコードから見える部分)と実装(コンパイル時に動作する部分)が明確に分離されています。 > **推奨される構成** > > 最低でも 3 つのファイルが必要です。宣言用の MyMacros.swift、実装用の MyMacrosPlugin.swift、テスト用の MyMacrosTests.swift です。この分離により保守性が向上します。 ## 式マクロの作成 式マクロはコード内でそのまま使える値を生成します。次は、変数名を含むカスタムエラーメッセージ付きで Optional をアンラップする `#unwrap` マクロの作成例です。 ### マクロの宣言 ```swift // MyMacros.swift 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(_ value: T?) -> T = #externalMacro( module: "MyMacrosPlugin", type: "UnwrapMacro" ) ``` このシグネチャは、マクロが Optional を受け取り、非 Optional の値を返すことを示しています。`#externalMacro` はプラグイン内の実装を指します。 ### swift-syntax を用いた実装 ```swift // UnwrapMacro.swift 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)" } } } ``` `expansion` メソッドはマクロ呼び出しを表す AST ノードとコンパイルコンテキストを受け取り、生成されたコードを含む `ExprSyntax` を返します。 ### プラグインの登録 ```swift // MyMacrosPlugin.swift 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 ] } ``` このエントリーポイントにより、プラグインで提供されるマクロをコンパイラに伝えます。 ## attached の member マクロの作成 member マクロは既存の型にメンバー(プロパティ、メソッド、ネスト型)を追加します。次は、ストアドプロパティすべてを含むイニシャライザを自動生成する `@AutoInit` マクロです。 ### 宣言と実装 ```swift // MyMacros.swift /// Automatically generates an initializer with all stored properties @attached(member, names: named(init)) public macro AutoInit() = #externalMacro( module: "MyMacrosPlugin", type: "AutoInitMacro" ) ``` ```swift // AutoInitMacro.swift 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 } } ``` ### AutoInit マクロの使用例 ```swift // UserModel.swift @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 value ``` このマクロはイニシャライザのボイラープレートを取り除いてくれるため、プロパティが多いデータモデルで特に効果を発揮します。 ## async を生成するための attached peer マクロ peer マクロは注釈された宣言と同じ階層に新しい宣言を追加します。次は、completion handler を使う関数の async 版を生成する `@AddAsync` マクロです。 ```swift // MyMacros.swift /// Automatically generates an async version of a function with completion handler @attached(peer, names: suffixed(Async)) public macro AddAsync() = #externalMacro( module: "MyMacrosPlugin", type: "AddAsyncMacro" ) ``` ```swift // AddAsyncMacro.swift 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" } } ``` ### AddAsync マクロのデモ ```swift // NetworkService.swift class NetworkService { @AddAsync func fetchUser( id: Int, completion: @escaping (Result) -> 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) ``` > **peer マクロの制約** > > 生成される関数名は `@attached` 属性の `names:` で宣言する必要があります。ここでは `suffixed(Async)` を指定しており、生成される関数は元の関数名に「Async」が付与された名前になります。 ## マクロの単体テスト マクロは生成したコードがコンパイルされるため、テストが欠かせません。Swift では `SwiftSyntaxMacrosTestSupport` がテストを支援します。 ```swift // MyMacrosTests.swift 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 ) } } ``` このテストでは正しいコード展開と、不適切な使用時の適切なエラーメッセージの両方を確認します。 ## 高度なマクロ:Observable プロパティラッパー このマクロは複数のロールを組み合わせ、自動通知付きのプロパティ監視システムを構築します。 ```swift // MyMacros.swift /// Adds automatic property change observation @attached(accessor) @attached(peer, names: prefixed(_)) public macro Observable() = #externalMacro( module: "MyMacrosPlugin", type: "ObservableMacro" ) ``` ```swift // ObservableMacro.swift 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] } } ``` ### Observable パターンの利用 ```swift // ViewModel.swift @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 は Swift 5.9 以降の新しい Observation フレームワークでも、同じパターンを採用しています。 ## マクロのデバッグと検査 Xcode にはマクロをデバッグし、生成されたコードを把握するための複数のツールが用意されています。 ### Xcode での展開 ```swift // DebuggingMacros.swift // 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 debugging ``` ### 開発中のロギング ```swift // DebugMacro.swift 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" } } ``` ### swift-ast-explorer で AST を観察する オンラインツール [swift-ast-explorer.com](https://swift-ast-explorer.com) では、任意の Swift コードの構文木を視覚的に確認できます。マクロ実装時に AST のノードをどのように辿るかを理解するうえで欠かせません。 ## Swift Macros のベストプラクティス 保守しやすいマクロを作るには、いくつかの規約を守り、よくある落とし穴を避ける必要があります。 ### バリデーションとエラーメッセージ ```swift // ValidationBestPractices.swift 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) } } ``` ### 読みやすいコードを生成する ```swift // ReadableCodeGeneration.swift // ❌ 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 } """ ``` 生成されるコードは「Expand Macro」で開発者が確認するため、手書きのコードと同じくらい読みやすくする必要があります。 ## まとめ Swift Macros は静的型安全性を保ったままボイラープレートを取り除く強力な手段です。この技術により次のことが実現できます。 **重要なポイント:** - ✅ 2 つのカテゴリ:freestanding(`#`)と attached(`@`) - ✅ attached の 5 つのロール:peer、accessor、member、memberAttribute、conformance - ✅ swift-syntax と AST 操作による実装 - ✅ `SwiftSyntaxMacrosTestSupport` を用いた必須のテスト - ✅ 実装のための専用パッケージが必要 - ✅ Xcode の「Expand Macro」によるデバッグ - ✅ 開発者体験に欠かせない明確なエラーメッセージ Swift Macros は Equatable や Codable などの準拠の生成、高度なプロパティラッパーの作成、コールバックベースの API を async/await へモダナイズする際に特に役立ちます。 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/ios/swift-macros-practical-metaprogramming-examples