如何使用Xcode创建Swift宏15
#发展 #ios #swift #apple

什么是宏?

Swift Macros允许您在编译时间生成重复代码,从而使您的应用程序的代码库更易于阅读,并且不太繁琐。

有两种类型的宏:

  • 自由主义宏代替代码中的其他内容。它们始终以主题标签(#)符号开始。
#caseDetection // Freestanding Macro
  • 附加宏用作代码声明的属性。它们以@标志开始。
@CaseDetection // Attached Macro

创建新的宏

需要在依赖swift-syntax库的特殊Package中创建宏。

SwiftSntax是一组Swift库,用于解析,检查,生成和转换Swift源代码。这是GitHub repo

创建一个新的宏转到New -> Package并选择Swift Macro
键入宏的名称并创建Package

仅键入宏的实际名称,而无需宏后缀。例如。对于一个名为 addAsync 的宏,类型 addAsync 不是 addasyncmacro

宏软件包结构

在新创建的Package中,您会找到一些自动生成的文件:

  • [Macro name].swift您声明宏的签名
  • main.swift您可以在其中测试宏的行为
  • [Macro name]Macro.swift您在其中编写宏的实际实现
  • [Macro name]Tests.swift您编写宏实现的测试

宏观角色

单个宏可以具有多个角色来定义其行为。
可用角色是:

@freestanding(表达式)

创建一个返回值的代码。

协议

ExpressionMacro

宣言

@freestanding(expression)

@freestanding(声明)

创建一个或多个声明。像structfunctionvariabletype

协议

DeclarationMacro

宣言

@freestanding (declaration, names: arbitrary)

@Attached(同行)

在应用于。

协议

PeerMacro

宣言

@attached(peer, names: overloaded)

@Attached(登录)

将访问者添加到属性。例如。将getset添加到var中。例如,swiftui中的@State

协议

AccessorMacro

宣言

@attached(accessor)

@Attached(成员Attribute)

在应用于。

的类型/扩展名中的声明中添加属性

协议

MamberAttributeMacro

宣言

@attached(memberAttribute)

@Attached(会员)

添加了应用于应用的类型/扩展中的新声明。例如。在struct中添加一个自定义的init()

协议

MemberMacro

宣言

@attached(member, names: named(init()))

@Attached(符合)

添加了协议的符合。

协议

ConformanceMacro

宣言

@attached(conformance)

构建宏

签名

在本指南中,我们将创建一个,该从completion One创建async功能。
要开始构建此宏,我们需要创建宏签名
为此,请转到[Macro name].swift文件并添加。

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

在这里,您可以声明宏(AddAsync)的名称,然后在#externalMacro中指定它所在的模块和宏的类型。

执行

然后实现实际宏,转到[Macro name]Macro.swift文件。
创建一个以宏名称命名的public struct,并根据您在Macro signature中指定的签名添加一致性。
因此,在新创建的struct内部添加所需的方法。

public struct AddAsyncMacro: PeerMacro {

   public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
      // Implement macro
   }
}

如果您的Macro signature具有多个角色,则需要添加每个角色,例如:

// Signature
@attached(accessor)
@attached(memberAttribute)
@attached(member, names: named(init()))
public macro // ...

// Implementation
public struct MyMacro: AccessorMacro, MamberAttributeMacro, MemberMacro { }

要知道相应的协议,请参阅宏角色部分。

导出宏

[Macro name]Macro.swift文件中添加或编辑此代码中的新创建的宏。

@main
struct AddAsyncMacroPlugin: CompilerPlugin {
  let providingMacros: [SwiftSyntaxMacros.Macro.Type] = [
    AddAsyncMacro.self
  ]
}

扩展方法

扩展方法负责生成隐藏的代码。
在这里,代码 acro declaration)已连接成碎片(TokenSyntax),并操纵以生成所需的附加代码。

要这样做,我们必须将declaration施加到所需的语法中。
例如。如果可以将宏连接到struct,我们将其施放到StructDeclSyntax
在这种情况下,宏只能连接到function上 因此,在expansion方法中添加:

guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else {
   // TODO: Throw error
}

return []

现在,在我们继续之前,我们需要编写一个测试,以检查宏的实现是否生成了我们期望的代码。
因此,在[Macro name]Tests.swift文件中添加:

func test_AddAsync() {
    assertMacroExpansion(
        """
        @AddAsync
        func test(arg1: String, completion: (String?) -> Void) {

        }
        """,
        expandedSource: """

        func test(arg1: String, completion: (String?) -> Void) {

        }

        func test(arg1: String) async -> String? {
          await withCheckedContinuation { continuation in
            self.test(arg1: arg1) { object in
              continuation.resume(returning: object)
            }
          }
        }
        """,
        macros: testMacros
    )
}

一旦到位,让我们在expansion中的return []添加一个断点并运行测试。
一旦我们碰到断点,请在调试控制台内运行po functionDecl以获取这一长描述:

FunctionDeclSyntax
├─attributes: AttributeListSyntax
│ ╰─[0]: AttributeSyntax
│   ├─atSignToken: atSign
│   ╰─attributeName: SimpleTypeIdentifierSyntax
│     ╰─name: identifier("AddAsync")
├─funcKeyword: keyword(SwiftSyntax.Keyword.func)
├─identifier: identifier("test")
├─signature: FunctionSignatureSyntax
│ ╰─input: ParameterClauseSyntax
│   ├─leftParen: leftParen
│   ├─parameterList: FunctionParameterListSyntax
│   │ ├─[0]: FunctionParameterSyntax
│   │ │ ├─firstName: identifier("arg1")
│   │ │ ├─colon: colon
│   │ │ ├─type: SimpleTypeIdentifierSyntax
│   │ │ │ ╰─name: identifier("String")
│   │ │ ╰─trailingComma: comma
│   │ ╰─[1]: FunctionParameterSyntax
│   │   ├─firstName: identifier("completion")
│   │   ├─colon: colon
│   │   ╰─type: FunctionTypeSyntax
│   │     ├─leftParen: leftParen
│   │     ├─arguments: TupleTypeElementListSyntax
│   │     │ ╰─[0]: TupleTypeElementSyntax
│   │     │   ╰─type: OptionalTypeSyntax
│   │     │     ├─wrappedType: SimpleTypeIdentifierSyntax
│   │     │     │ ╰─name: identifier("String")
│   │     │     ╰─questionMark: postfixQuestionMark
│   │     ├─rightParen: rightParen
│   │     ╰─output: ReturnClauseSyntax
│   │       ├─arrow: arrow
│   │       ╰─returnType: SimpleTypeIdentifierSyntax
│   │         ╰─name: identifier("Void")
│   ╰─rightParen: rightParen
╰─body: CodeBlockSyntax
  ├─leftBrace: leftBrace
  ├─statements: CodeBlockItemListSyntax
  ╰─rightBrace: rightBrace

在这里您可以看到函数声明的每个组件。
现在,您可以选择所需的单独作品并使用它来创建宏观生成的代码。

检索第一个参数名称

例如,如果您需要检索函数的第一个参数名称,则将写下:

let signature = functionDecl.signature.as(FunctionSignatureSyntax.self)
let parameters = signature?.input.parameterList
let firstParameter = parameters?.first
let parameterName = firstParameter.firstName // -> arg1

这是一个很长的代码,只是为了检索一个字符串,如果您需要处理多个功能变体,它将更加复杂。
我认为苹果将来会改善这一点,但是现在让我们坚持下去。

完成实施

现在,让我们完成AddAsync实现。

if let signature = functionDecl.signature.as(FunctionSignatureSyntax.self) {
   let parameters = signature.input.parameterList

   // 1.
   if let completion = parameters.last,
      let completionType = completion.type.as(FunctionTypeSyntax.self)?.arguments.first,
   let remainPara = FunctionParameterListSyntax(parameters.removingLast()) {

   // 2. returns "arg1: String"
   let functionArgs = remainPara.map { parameter -> String in
      guard let paraType = parameter.type.as(SimpleTypeIdentifierSyntax.self)?.name else { return "" }
      return "\(parameter.firstName): \(paraType)"
   }.joined(separator: ", ")

   // 3. returns "arg1: arg1"
   let calledArgs = remainPara.map { "\($0.firstName): \($0.firstName)" }.joined(separator: ", ")

   // 4.
   return [
   """
   func \(functionDecl.identifier)(\(raw: functionArgs)) async -> \(completionType) {
      await withCheckedContinuation { continuation in
         self.\(functionDecl.identifier)(\(raw: calledArgs)) { object in
            continuation.resume(returning: object)
         }
      }
   }
   """
   ]
}

在这个代码块中,我们:

  1. 从函数签名中检索完成参数
  2. 解析函数参数,除了完成
  3. 创建传递到称为函数的参数
  4. 组成异步函数

显示自定义错误

宏使您可以向用户显示自定义错误。
例如,如果用户将宏放置在struct上,但是该宏只能与functions一起使用。
在这种情况下,您可以丢弃错误,并且将在Xcode中自动显示。

enum AsyncError: Error, CustomStringConvertible {

  case onlyFunction

  var description: String {
    switch self {
    case .onlyFunction:
      return "@AddAsync can be attached only to functions."
    }
  }
}

// Inside `expansion` method. 
guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else {
   throw AsyncError.onlyFunction // <- Error thrown here
}

测试宏使用

测试AddAsync宏的行为。
转到main.swift文件并添加:

struct AsyncFunctions {

  @AddAsync
  func test(arg1: String, completion: (String) -> Void) {

  }
}

func testing() async {
  let result = await AsyncFunctions().test(arg1: "Blob")
}

您可以看到构建成功完成。
!警告自动完成可能不会显示生成的异步功能。

显示宏生成的代码

在代码中展开宏,并在宏中查看自动生成的代码,right click,然后从菜单中选择Expand Macro

断点

宏产生的代码可以通过像往常一样添加断点来调试。
为此,请在宏观上right click,然后从菜单中选择Expand Macro
然后在要调试的线路上添加一个断点。

结论

恭喜!您刚刚创建了第一个宏。
查看here完整的代码。
如您所见,到目前为止,宏实现可能是执行简单任务的很长的事件。
但是,一旦您缠绕着头,它们就会非常有用,并且可以节省很多样板代码。
宏仍处于beta状态,所以我认为苹果会在公开发售时会改善它们。
感谢您阅读