SwiftUI或关于功能构建器的魔力


您是否尝试过向VStack添加10个以上的视图?


 var body: some View { VStack { Text("Placeholder1") Text("Placeholder2") // ...    3  10 . . . Text("Placeholder11") } } 

我试过了-它不能编译。 是的,我一开始也很惊讶,并投入到Swift论坛和github的研究中。 我的研究结果是-“它仍然无法编译¯\_(ツ)_/¯ ”。 但是,等等,让我们看看为什么。


功能构建器


首先,有必要了解如何普遍使用这种语法。 这样的声明式创建元素的基础对我们来说很罕见,那就是Function Builder机制。
swift-evolution的github上,有John McCall和Doug Gregor提出的建议- 函数构建器(建议:SE-XXXX) ,其中详细描述了他们面临的问题,为什么决定使用函数构建器以及它是什么。这样的。


那是什么


简而言之很难对此进行描述,但是总而言之-这是一种机制,它允许您列出参数,宝藏中的某些内容,并从所有这些中得出总体结果。
提案报价:SE-XXXX


主要思想是我们采用表达式的结果,包括诸如if和switch之类的嵌套表达式,并将它们形成一个结果,该结果成为当前函数的返回值。 此“构建”由功能构建器控制,该功能构建器是自定义属性。

原来的
基本思想是,我们采用语句块的“忽略”表达式结果-包括嵌套位置(如ifandswitch语句的主体)-并将它们构建为单个结果值,该结果值成为当前函数的返回值。 此收集的执行方式由功能构建器控制,这只是一种新的自定义属性类型;

这种机制允许您编写声明性的类似树的代码,而不必使用不必要的标点符号:


 let myBody = body { let chapter = spellOutChapter ? "Chapter" : "" div { if useChapterTitles { h1(chapter + "1. Loomings.") } p { "Call me Ishmael. Some years ago" } p { "There is now your insular city" } } } 

这要归功于新的@_functionBuilder属性。 该属性标记了一些构建器,它可以是结构。 该构建器实现了许多特定的方法。 此外,此构建器本身在各种情况下都用作用户属性。
下面,我将展示其工作原理以及如何组织此类代码。


怎么会这样


因此,Apple希望对内置的特定域的语言 DSL提供支持
John McCall和Doug Gregor指出,这样的代码更易于阅读和编写-简化了语法,使其更加简洁,因此,该代码得到了更多的支持。 同时,他们注意到他们的解决方案不是通用DSL。
该解决方案着重于一组特定的问题,包括描述线性和树形结构,例如XML,JSON,视图层次结构等。


如何使用它?


您可以创建自己的函数生成器,这对我来说更容易理解它是如何工作的。 考虑连接字符串的构建器的原始示例。


 // 1.  Builder @_functionBuilder struct MyBuilder { static func buildBlock(_ atrs: String...) -> String { return atrs.reduce("", + ) } } 

 // 2.          func stringsReduce(@MyBuilder block: () -> String) -> String { return block() } 

 // 3.     let result = stringsReduce { "1" "2" } print(result) // "12" 

在幕后,这将如下所示:


 let result = stringsReduce { return MyBuilder.build("1", "2") } 

重要的是,在构建器的实现中,方法必须是完全静态的,具有此列表中的特定名称和特定类型的参数 。 您只能更改输入参数的类型和名称。


 static func buildBlock(_ <*atrs*>: <*String*>...) -> <*String*> 

特定的方法名称将在构建器中搜索,并在编译阶段替换。 如果找不到该方法,则会发生编译错误。
这是魔术。 当您实现构建器时,编译器将不会告诉您任何信息。 它不会说明可用的方法,也不会自动完成。 仅当您编写此构建器无法处理的客户端代码时,您才会得到模糊的错误。
到目前为止,我发现的唯一解决方案是以一系列方法为指导。
那么为什么我们需要其他方法呢? 好吧,例如,通过检查来支持此类代码


 stringsReduce { if .random() { //   Bool "one string" } else { "another one" } "fixed string" } 

为了支持此语法,构建器需要实现buildEither(first:/second:)方法


 static func buildEither(first: String) -> String { return first } static func buildEither(second: String) -> String { return second } 

社区回应


有趣的是,这在Swift 5.1中还没有,也就是说,尚未提出具有此功能的拉取请求 ,但是,Apple已将其添加到Xcode 11 beta中。 在功能构建器→音调→快捷论坛上,您可以看到社区对此建议的反应。


ViewBuilder


现在回到VStack并查看其初始化程序init的文档(alignment: VStack :content :)
看起来像这样:


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

在内容表之前有一个自定义属性@ViewBuilder
声明如下:


 @_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 } 

用户属性是@ _functionBuilder ,该属性在其声明的开始处注册。


而且,如果您浏览的文档甚至更低,您会看到许多静态的buildBlock方法,它们的参数数量有所不同。


这意味着视图代码


 var body: some View { VStack { Text("Placeholder1") Text("Placeholder2") Text("Placeholder3") } } 

在引擎盖下转换成这样


  var body: some View { VStack(alignment: .leading) { ViewBuilder.buildBlock(Text("Placeholder1"), Text("Placeholder2"), Text("Placeholder3")) } } 

即 完成buildBlock (:: _ :)构建器方法。


从整个列表中,具有最大参数数量的方法是这个家伙buildBlock(::::::::: :) :) (10个参数):


 extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View } 

因此,返回到原始示例,当您尝试在内部提升VStack和11个视图时,编译器将尝试查找具有11个输入参数的ViewBuilder'a buildBlock方法。 但是没有这样的方法:因此出现编译错误。
对于在初始化器中使用@ViewBuilder属性进行初始化的所有集合,都是这样:V | H | Z-Stack,List,Group等,在其中可以声明多个视图作为枚举。
这太可悲了。


MEM (对不起,我没有找到像样的模因)


如何成为


我们可以使用ForEach规避此限制ForEach


 struct TestView : View { var body: some View { VStack { ForEach(texts) { i in Text(«\(i)») } } } var texts: [Int] { var result: [Int] = [] for i in 0...150 { result.append(i) } return result } } 

或嵌套集合:


 var body: some View { VStack { VStack { Text("Placeholder_1") Text("Placeholder_2") //   8 } Group { Text("11") Text("12") //   8 } } } 

但是,这样的决定看起来像拐杖,只有希望拥有更光明的未来。 但是这个未来是什么样的?
Swift已经具有可变参数 。 这是一种方法通过枚举接受参数的能力。 例如,众所周知的print方法允许您同时写入print(1, 2)print(1, 2, 3, 4)而这不会不必要地重载该方法。


 print(items: Any...) 

但是这种语言的功能还不够,因为buildBlock方法接受不同的通用参数作为输入。
添加可变参数泛型将解决此问题。 可变参数泛型允许您从许多泛型类型中抽象出来,例如,如下所示:


  static func buildBlock<…Component>(Component...) -> TupleView<(Component...)> where Component: View 

苹果只是有义务添加它。 现在,一切都与这种机制背道而驰。 在我看来,他们根本没有设法在WWDC 2019之前完成它(但这只是猜测)。

Source: https://habr.com/ru/post/zh-CN455760/


All Articles