-
Write Swift macros
Discover how you can use Swift macros to make your codebase more expressive and easier to read. Code along as we explore how macros can help you avoid writing repetitive code and find out how to use them in your app. We'll share the building blocks of a macro, show you how to test it, and take you through how you can emit compilation errors from macros.
Chapitres
- 1:15 - Overview
- 5:10 - Create a macro using Xcode's macro template
- 10:50 - Macro roles
- 11:40 - Write a SlopeSubset macro to define an enum subset
- 20:17 - Inspect the syntax tree structure in the debugger
- 24:35 - Add a macro to an Xcode project
- 27:05 - Emit error messages from a macro
- 30:12 - Generalize SlopeSubset to a generic EnumSubset macro
Ressources
Vidéos connexes
WWDC23
-
Rechercher dans cette vidéo…
-
-
5:55 - Invocation of the stringify macro
import WWDC let a = 17 let b = 25 let (result, code) = #stringify(a + b) print("The value \(result) was produced by the code \"\(code)\"") -
6:31 - Declaration of the stringify macro
@freestanding(expression) public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "WWDCMacros", type: "StringifyMacro") -
7:10 - Implementation of the stringify macro
public struct StringifyMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) -> ExprSyntax { guard let argument = node.argumentList.first?.expression else { fatalError("compiler bug: the macro does not have any arguments") } return "(\(argument), \(literal: argument.description))" } } -
9:12 - Tests for the stringify Macro
final class WWDCTests: XCTestCase { func testMacro() { assertMacroExpansion( """ #stringify(a + b) """, expandedSource: """ (a + b, "a + b") """, macros: testMacros ) } } let testMacros: [String: Macro.Type] = [ "stringify": StringifyMacro.self ] -
12:05 - Slope and EasySlope
/// Slopes in my favorite ski resort. enum Slope { case beginnersParadise case practiceRun case livingRoom case olympicRun case blackBeauty } /// Slopes suitable for beginners. Subset of `Slopes`. enum EasySlope { case beginnersParadise case practiceRun init?(_ slope: Slope) { switch slope { case .beginnersParadise: self = .beginnersParadise case .practiceRun: self = .practiceRun default: return nil } } var slope: Slope { switch self { case .beginnersParadise: return .beginnersParadise case .practiceRun: return .practiceRun } } } -
14:16 - Declare SlopeSubset
/// Defines a subset of the `Slope` enum /// /// Generates two members: /// - An initializer that converts a `Slope` to this type if the slope is /// declared in this subset, otherwise returns `nil` /// - A computed property `slope` to convert this type to a `Slope` /// /// - Important: All enum cases declared in this macro must also exist in the /// `Slope` enum. @attached(member, names: named(init)) public macro SlopeSubset() = #externalMacro(module: "WWDCMacros", type: "SlopeSubsetMacro") -
15:24 - Write empty implementation for SlopeSubset
/// Implementation of the `SlopeSubset` macro. public struct SlopeSubsetMacro: MemberMacro { public static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [] } } -
16:23 - Register SlopeSubsetMacro in the compiler plugin
@main struct WWDCPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ SlopeSubsetMacro.self ] } -
18:41 - Test SlopeSubset
let testMacros: [String: Macro.Type] = [ "SlopeSubset" : SlopeSubsetMacro.self, ] final class WWDCTests: XCTestCase { func testSlopeSubset() { assertMacroExpansion( """ @SlopeSubset enum EasySlope { case beginnersParadise case practiceRun } """, expandedSource: """ enum EasySlope { case beginnersParadise case practiceRun init?(_ slope: Slope) { switch slope { case .beginnersParadise: self = .beginnersParadise case .practiceRun: self = .practiceRun default: return nil } } } """, macros: testMacros ) } } -
19:25 - Cast declaration to an enum declaration
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { // TODO: Emit an error here return [] } -
21:14 - Extract enum members
let members = enumDecl.memberBlock.members -
21:32 - Load enum cases
let caseDecls = members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) } -
21:58 - Retrieve enum elements
let elements = caseDecls.flatMap { $0.elements } -
24:11 - Generate initializer
let initializer = try InitializerDeclSyntax("init?(_ slope: Slope)") { try SwitchExprSyntax("switch slope") { for element in elements { SwitchCaseSyntax( """ case .\(element.identifier): self = .\(element.identifier) """ ) } SwitchCaseSyntax("default: return nil") } } -
24:19 - Return generated initializer
return [DeclSyntax(initializer)] -
25:51 - Apply SlopeSubset to EasySlope
/// Slopes suitable for beginners. Subset of `Slopes`. @SlopeSubset enum EasySlope { case beginnersParadise case practiceRun var slope: Slope { switch self { case .beginnersParadise: return .beginnersParadise case .practiceRun: return .practiceRun } } } -
28:00 - Test that we generate an error when applying SlopeSubset to a struct
func testSlopeSubsetOnStruct() throws { assertMacroExpansion( """ @SlopeSubset struct Skier { } """, expandedSource: """ struct Skier { } """, diagnostics: [ DiagnosticSpec(message: "@SlopeSubset can only be applied to an enum", line: 1, column: 1) ], macros: testMacros ) } -
28:48 - Define error to emit when SlopeSubset is applied to a non-enum type
enum SlopeSubsetError: CustomStringConvertible, Error { case onlyApplicableToEnum var description: String { switch self { case .onlyApplicableToEnum: return "@SlopeSubset can only be applied to an enum" } } } -
29:09 - Throw error if SlopeSubset is applied to a non-enum type
throw SlopeSubsetError.onlyApplicableToEnum -
31:03 - Generalize SlopeSubset declaration to EnumSubset
@attached(member, names: named(init)) public macro EnumSubset<Superset>() = #externalMacro(module: "WWDCMacros", type: "SlopeSubsetMacro") -
31:33 - Retrieve the generic parameter of EnumSubset
guard let supersetType = attribute .attributeName.as(SimpleTypeIdentifierSyntax.self)? .genericArgumentClause? .arguments.first? .argumentType else { // TODO: Handle error return [] }
-