SwiftUI刨根问底之你凭啥可以这么写?

288次阅读  |  发布于3年以前

var body: some View {
    HStack(alignment: .leading, spacing: 10) {
        Text("Hello Swift")
        Text("Hello SwiftUI")
        Text("Hello SwiftUI Study")
    }
}

不知道大家有没有问过自己,为啥在SwiftUI中可以如上面这样写?

实际上可以像上面这样去使用,得益于两个特性:Swift的尾随闭包 & FunctionBuilder。

尾随闭包

我们查看HStack的定义,发现它的init初始化函数定义如下:

@inlinable public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil,  content: () -> Content)

在init函数的定义中,最后一项是一个闭包content: () -> Content ,在Swift语法中我们知道,如果函数的最后一个参数是一个闭包,那么可以把这个闭包提到圆括号外面,那上面的代码本来应该是这样:

var body: some View {
    HStack(alignment: .leading, spacing: 10, content: {
        Text("Hello Swift")
        Text("Hello SwiftUI")
        Text("Hello SwiftUI Study")
    })
}

这就是Swift中的其中一种闭包:尾随闭包。

另外在Swift中,如果某个函数只有一个参数,且这个参数是一个闭包或者这个闭包参数之前参数提供了默认值的话,可以完全省略圆括号。在上面init函数的定义中我们看到尾随闭包之前的参数都提供了默认值,所以SwiftUI中也有这样的写法:

var body: some View {
    HStack {
        Text("Hello Swift")
        Text("Hello SwiftUI")
        Text("Hello SwiftUI Study")
    }
}

FunctionBuilder

但你肯定还会有疑问,上面init函数的尾随闭包,明明是有返回值Content,可是在使用HStack的地方,为什么没有任何的return呢?而且如果你尝试下面这样的写法,编译器是要报错的,所以这个闭包可能不是一个简单的闭包:

var body: some View {
    HStack {
        let a = 1
        print(a)
        Text("Hello Swift")
        Text("Hello SwiftUI")
        Text("Hello SwiftUI Study")
    }
}

其实上面init函数的定义,我们省略了一些内容,原始是这样的:

@inlinable public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)

没错,就是多了一个@ViewBuilder修饰闭包,那我们就知道一定是这个家伙让我们的闭包可以没有返回值,它是何方神圣,会拥有此等魔力?一起探究一下!

首先进到ViewBuilder的定义,可以看到ViewBuilder其实是一个struct:

@_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) -> Content where Content : View
}

我们注意到,ViewBuilder本身又是被@_functionBuilder所修饰的,这就是我们的主角了。这个语法特性叫Function Builders

这是苹果为SwiftUI直接修改了Swift的编译器,还没有被正式添加到Swift语法中,所以只有在SwiftUI中使用。

@_functionBuilder作为标记,可以作为各种类型的自定义Attribute,比如函数,计算型属性,以及上面提到的作为参数的函数(闭包),使用@_functionBuilder标记之后,会修改语法树,针对不同的类型会有不同的转换:

再回到上面的ViewBuilder,使用@_functionBuilder标记之后,会转换闭包的实现为ViewBuilder的实现,在之前的文章中也介绍过,ViewBuilder中实现了1到10个View的buildBlock:

@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

    // ...
}

于是

HStack {
   Text("Hello Swift")
   Text("Hello SwiftUI")
   Text("Hello SwiftUI Study")
}

相当于

HStack {
    ViewBuilder.buildBlock(
        Text("Hello Swift"),
        Text("Hello SwiftUI"),
        Text("Hello SwiftUI Study"),
    )
}

在ViewBuilder中还实现了返回条件内容的函数:

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
}

这些函数提供了条件处理的实现:

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

最终会被转换为

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

怎么样,是不是挺神奇?其实Funcation builder这个语言特性,在我们平时用不到,但是在写DSL时就会非常有用,感兴趣的同学可以去看看相关实现哦。

好了,今天从HStack的写法,为你介绍了尾随闭包和function builder语言特性,希望你能有收获!每天进步一点点,成为更好的自己!

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8