📜 ⬆️ ⬇️

Magic SwiftUI or Function builders


Have you tried to add more than 10 views to VStack?


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

I tried it - it does not compile. Yes, I, too, was initially surprised and plunged into the study of the Swift forum and the github. The result of my study was - "still does not compile ¯\_(ツ)_/¯ ". But wait, let's see why.


Function builder


For a start it is worth understanding how such syntax became generally available. At the base of the declarative element creation, which is so unusual for us, lies the Function Builder mechanism.
There is a proposal by John McCall and Doug Gregor - Function builders (Proposal: SE-XXXX) on github in swift-evolution , in which they describe in detail what problem they faced, why it was decided to use Functions Builder and what it is such


So what is it?


It is difficult to describe it in a nutshell, but if it is short, it is a mechanism that allows you to enumerate arguments, some content in the body of a clash, and to give a general result from all this.
Proposal quote : SE-XXXX :


The basic idea is that we take the result of an expression, including nested expressions like if and switch, and form them into one result, which becomes the return value of the current function. This "build" is controlled by a function builder, which is a custom attribute.

Original
It is a rule of thumb for a statement. This is a collection of custom-attribute type;

This mechanism allows you to write a declarative tree, without any extra punctuation:


 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" } } } 

This is available thanks to the new @_functionBuilder attribute. This attribute marks some builder, it can be a structure. This builder implements a number of specific methods. Further, this builder is used by itself, as a user attribute in various situations.
Below, I will show you how it works and how to organize such code.


Why is this?


Thus, Apple wants to support the integrated domain-specific language DSL .
John McCall and Doug Gregor cite the main arguments that such code is much easier to read and write - this simplifies the syntax, makes it more concise and, as a result, the code becomes more supported. However, they note that their solution is not a universal DSL.
This solution is aimed at a specific set of problems, including the description of linear and tree structures, such as XML, JSON, View hierarchies, etc.


How to work with it?


You can create your function builder, it was easier for me to understand how it works that way. Consider a primitive example of a builder that concatenates strings.


 // 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" 

Under the hood, it will work like this:


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

It is important that in the implementation of the builder, the methods must be static, with specific names and with a specific type of parameters from this list . You can only change the type and name of the input parameter.


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

Specific method names will be searched for in the builder and substituted at the compilation stage. And if the method is not found, a compilation error will occur.
And this is magic. When you implement the builder, the compiler does not tell you absolutely nothing. Does not tell about the available methods, does not help autocomplete. Only when you write a client code that cannot be handled by this builder will you get a vague error.
So far the only solution I have found is to follow the list of methods .
So why do we need other methods? Well, for example, to support such a code with checks


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

To support this syntax in the builder, you need to implement the buildEither(first:/second:) methods buildEither(first:/second:)


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

Community response


The funny thing is that this is not yet in Swift 5.1, that is, the pull-request with this feature is not yet infused, but nevertheless Apple has already added it to XCode 11 beta. And on Function builders → Pitches → Swift Forums, you can see the reaction of the community to this proposal.


Viewbuilder


Now let's go back to VStack and see the documentation of its init init (alignment: spacing: content :) .
It looks like this:


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

And the content is facing the custom attribute @ViewBuilder
He announced as follows:


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

Its custom attribute makes it the @ _functionBuilder , written at the beginning of its declaration.


And if you look through the documentation even lower, then you can see a lot of static buildBlock methods that differ in the number of arguments.


This means that the view code


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

under the hood converted to such


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

Those. fulfills the buildBlock builder method (:: _ :) .


From this entire list, the method with the maximum number of arguments is the buildBlock guy (:::::::::: :) (10 arguments):


 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 } 

And accordingly, returning to the original example, when you try to raise the VStack and eleven views inside, the compiler tries to find the ViewBuilder method buildBlock , which has 11 arguments as input. But there is no such method: hence the compilation error.
This is relevant for all collections that use an @ViewBuilder attribute in the initializer: V | H | Z-Stack, List, Group and others, within which you can declare more than one view as an enumeration.
And this is sad.


MEM (sorry, never found a decent meme)


How to be?


We can bypass this restriction using 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 } } 

Or nesting collections:


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

But such solutions look like crutches and only hope for a bright future remains. But what kind of future is it?
Swift already has Variadic parameters . This is the ability of the method to accept arguments by enumeration. For example, the print method known to everyone allows you to write both print(1, 2) and print(1, 2, 3, 4) and this without unnecessary method overloads.


 print(items: Any...) 

But this feature of the language is not enough, since the buildBlock method accepts various generic arguments as input.
Adding Variadic generics would solve this problem. Variadic generics allow you to abstract from the set of generic types, for example, something like this:


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

And Apple just has to add it. Now everything is limited by this mechanism. And it seems to me that they simply did not have time to finish it for WWDC 2019 (but these are only conjectures).


')

Source: https://habr.com/ru/post/455760/


All Articles