什么使 SwiftUI 的 DSL 成为可能? [英] What enables SwiftUI's DSL?

查看:24
本文介绍了什么使 SwiftUI 的 DSL 成为可能?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

似乎 Apple 的新 SwiftUI 框架使用了一种新语法,可以有效地构建元组,但具有另一种语法:

It seems like Apple's new SwiftUI framework uses a new kind of syntax that effectively builds a tuple, but has another syntax:

var body: some View {
    VStack(alignment: .leading) {
        Text("Hello, World") // No comma, no separator ?!
        Text("Hello World!")
    }
}

试图解决这个语法的真正含义,我发现这里使用的 VStack 初始值设定项采用 () -> 类型的闭包.内容作为第二个参数,其中 Content 是符合 View 的通用参数,通过闭包推断.为了找出 Content 被推断为什么类型,我稍微更改了代码,保持其功能:

Trying to tackle down what this syntax really is, I found out that the VStack initializer used here takes a closure of the type () -> Content as the second parameter, where Content is a generic param conforming to View that is inferred via the closure. To find out what type Content is inferred to, I changed the code slightly, maintaining its functionality:

var body: some View {
    let test = VStack(alignment: .leading) {
        Text("Hello, World")
        Text("Hello World!")
    }

    return test
}

这样,test 显示自己是 VStack> 类型,这意味着 Content> 属于 TupleView 类型.查看 TupleView,我发现它是一个源自 SwiftUI 本身的包装器类型,只能通过传递它应该包装的元组来初始化.

With this, test reveals itself to be of type VStack<TupleView<(Text, Text)>>, meaning that Content is of type TupleView<Text, Text>. Looking up TupleView, I found it's a wrapper type originating from SwiftUI itself that can only be initialized by passing the tuple it should wrap.

问题

现在我想知道这个例子中的两个 Text 实例到底是如何转换为 TupleView<(Text, Text)> 的.这是否被入侵到 SwiftUI 中,因此 常规 Swift 语法无效? TupleView 作为 SwiftUI 类型支持这个假设.或者这是有效的Swift语法吗?如果是,如何SwiftUI之外使用它?

Now I'm wondering how in the world the two Text instances in this example are converted to a TupleView<(Text, Text)>. Is this hacked into SwiftUI and therefore invalid regular Swift syntax? TupleView being a SwiftUI type supports this assumption. Or is this valid Swift syntax? If yes, how can one use it outside SwiftUI?

推荐答案

AsMartin 说,如果您查看 VStack 的文档init(alignment:spacing:content:),可以看到content:参数有属性@ViewBuilder:

As Martin says, if you look at the documentation for VStack's init(alignment:spacing:content:), you can see that the content: parameter has the attribute @ViewBuilder:

init(alignment: HorizontalAlignment = .center, spacing: Length? = nil,
     @ViewBuilder content: () -> Content)

该属性指的是 ViewBuilder 类型,它如果你看一下生成的界面,看起来像:

This attribute refers to the ViewBuilder type, which if you look at the generated interface, looks like:

@_functionBuilder public struct ViewBuilder {

    /// Builds an empty view from an block containing no statements, `{ }`.
    public static func buildBlock() -> EmptyView

    /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`)
    /// through unmodified.
    public static func buildBlock(_ content: Content) -> Content 
      where Content : View
}

@_functionBuilder 属性是名为function builders",已经在这里介绍 Swift 的演变,并专门为 Xcode 11 附带的 Swift 版本实现,允许它在 SwiftUI 中使用.

The @_functionBuilder attribute is a part of an unofficial feature called "function builders", which has been pitched on Swift evolution here, and implemented specially for the version of Swift that ships with Xcode 11, allowing it to be used in SwiftUI.

标记类型 @_functionBuilder 允许将其用作各种声明的自定义属性,例如函数、计算属性以及在这种情况下函数类型的参数.此类带注释的声明使用函数构建器来转换代码块:

Marking a type @_functionBuilder allows it to be used as a custom attribute on various declarations such as functions, computed properties and, in this case, parameters of function type. Such annotated declarations use the function builder to transform blocks of code:

  • 对于带注释的函数,被转换的代码块是实现.
  • 对于带注释的计算属性,被转换的代码块是 getter.
  • 对于函数类型的带注释参数,被转换的代码块是传递给它的任何闭包表达式(如果有).

函数构建器转换代码的方式由其实现 builder 方法,例如 buildBlock,它采用一组表达式并将它们合并为一个值.

The way in which a function builder transforms code is defined by its implementation of builder methods such as buildBlock, which takes a set of expressions and consolidates them into a single value.

例如,ViewBuilder 为 1 到 10 个 View 符合参数实现了 buildBlock,将多个视图合并为一个 TupleView:

For example, ViewBuilder implements buildBlock for 1 to 10 View conforming parameters, consolidating multiple views into a single TupleView:

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

    /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`)
    /// through unmodified.
    public static func buildBlock<Content>(_ content: Content)
       -> Content where Content : View

    public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) 
      -> TupleView<(C0, C1)> where C0 : View, C1 : View

    public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2)
      -> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View

    // ...
}

这允许将传递给 VStack 初始化程序的闭包中的一组视图表达式转换为对 buildBlock 的调用,该调用采用相同数量的参数.例如:

This allows a set of view expressions within a closure passed to VStack's initialiser to be transformed into a call to buildBlock that takes the same number of arguments. For example:

struct ContentView : View {
  var body: some View {
    VStack(alignment: .leading) {
      Text("Hello, World")
      Text("Hello World!")
    }
  }
}

转化为对buildBlock(_:_:)的调用:

struct ContentView : View {
  var body: some View {
    VStack(alignment: .leading) {
      ViewBuilder.buildBlock(Text("Hello, World"), Text("Hello World!"))
    }
  }
}

导致 不透明的结果类型 some ViewTupleView<满足(文字,文字)>.

你会注意到 ViewBuilder 只定义了 buildBlock 最多 10 个参数,所以如果我们尝试定义 11 个子视图:

You'll note that ViewBuilder only defines buildBlock up to 10 parameters, so if we attempt to define 11 subviews:

  var body: some View {
    // error: Static member 'leading' cannot be used on instance of
    // type 'HorizontalAlignment'
    VStack(alignment: .leading) {
      Text("Hello, World")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
      Text("Hello World!")
    }
  }

我们收到编译器错误,因为没有构建器方法来处理此代码块(请注意,由于此功能仍在开发中,因此围绕它的错误消息不会有太大帮助).

we get a compiler error, as there's no builder method to handle this block of code (note that because this feature is still a work-in-progress, the error messages around it won't be that helpful).

实际上,我不相信人们会经常遇到这种限制,例如上面的例子使用 ForEach 视图:

In reality, I don't believe people will run into this restriction all that often, for example the above example would be better served using the ForEach view instead:

  var body: some View {
    VStack(alignment: .leading) {
      ForEach(0 ..< 20) { i in
        Text("Hello world \(i)")
      }
    }
  }

但是,如果您确实需要超过 10 个静态定义的视图,您可以使用 轻松解决此限制Group 视图:

If however you do need more than 10 statically defined views, you can easily workaround this restriction using the Group view:

  var body: some View {
    VStack(alignment: .leading) {
      Group {
        Text("Hello world")
        // ...
        // up to 10 views
      }
      Group {
        Text("Hello world")
        // ...
        // up to 10 more views
      }
      // ...
    }

<小时>

ViewBuilder 还实现了其他函数构建器方法,例如:


ViewBuilder also implements other function builder methods such:

extension ViewBuilder {
    /// Provides support for "if" statements in multi-statement closures, producing
    /// ConditionalContent for the "then" branch.
    public static func buildEither<TrueContent, FalseContent>(first: TrueContent)
      -> ConditionalContent<TrueContent, FalseContent>
           where TrueContent : View, FalseContent : View

    /// Provides support for "if-else" statements in multi-statement closures, 
    /// producing ConditionalContent for the "else" branch.
    public static func buildEither<TrueContent, FalseContent>(second: FalseContent)
      -> ConditionalContent<TrueContent, FalseContent>
           where TrueContent : View, FalseContent : View
}

这使它能够处理 if 语句:

This gives it the ability to handle if statements:

  var body: some View {
    VStack(alignment: .leading) {
      if .random() {
        Text("Hello World!")
      } else {
        Text("Goodbye World!")
      }
      Text("Something else")
    }
  }

转化为:

  var body: some View {
    VStack(alignment: .leading) {
      ViewBuilder.buildBlock(
        .random() ? ViewBuilder.buildEither(first: Text("Hello World!"))
                  : ViewBuilder.buildEither(second: Text("Goodbye World!")),
        Text("Something else")
      )
    }
  }

(为了清晰起见,向 ViewBuilder.buildBlock 发出冗余的 1 参数调用).

(emitting redundant 1-argument calls to ViewBuilder.buildBlock for clarity).

这篇关于什么使 SwiftUI 的 DSL 成为可能?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆