A mágica do SwiftUI ou sobre os construtores de funções


Você tentou adicionar mais de 10 visualizações ao VStack?


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

Eu tentei - não compila. Sim, também fiquei surpreso no começo e mergulhei no estudo do fórum Swift e do github. O resultado do meu estudo foi - "ele ainda não compila ¯\_(ツ)_/¯ ". Mas espere, vamos ver o porquê.


Construtor de funções


Para começar, vale a pena entender como essa sintaxe se tornou geralmente disponível. A base dessa criação declarativa de elementos tão incomuns para nós é o mecanismo do Function Builder .
No github em rápida evolução, há uma proposta de John McCall e Doug Gregor - Construtores de funções (Proposta: SE-XXXX) , na qual eles descrevem em detalhes qual problema estavam enfrentando, por que foi decidido usar o Functions Builder e o que era tal.


Então o que é isso?


É difícil descrevê-lo em poucas palavras, mas em resumo - este é um mecanismo que permite listar argumentos, algum conteúdo no corpo de um tesouro e fornecer um resultado geral de tudo isso.
Citação da proposta: SE-XXXX :


A idéia principal é que pegamos o resultado da expressão, incluindo expressões aninhadas como if e switch, e as transformamos em um resultado, que se torna o valor de retorno da função atual. Essa "construção" é controlada pelo construtor de funções, que é um atributo customizado.

O original
a idéia básica é que tomemos os resultados da expressão "ignorada" de um bloco de instruções - inclusive em posições aninhadas, como os corpos das declarações de comutadores - e os construímos em um único valor de resultado que se torna o valor de retorno da função atual. A maneira como essa coleção é executada é controlada pelo afunction builder, que é apenas um novo tipo de atributo personalizado;

Esse mecanismo permite escrever código declarativo do tipo árvore, sem caracteres de pontuação desnecessários:


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

Isso está disponível graças ao novo atributo @_functionBuilder . Este atributo marca algum construtor, pode ser uma estrutura. Este construtor implementa vários métodos específicos. Além disso, esse construtor é usado por si só, como um atributo do usuário em várias situações.
Abaixo, mostrarei como funciona e como organizar esse código.


Por que isso?


Portanto, a Apple quer dar suporte ao DSL de idioma específico do domínio incorporado.
John McCall e Doug Gregor apontam que esse código é muito mais fácil de ler e escrever - isso simplifica a sintaxe, torna-o mais conciso e, como resultado, o código se torna mais suportado. Ao mesmo tempo, eles observam que sua solução não é uma DSL universal.
Esta solução se concentra em um conjunto específico de problemas, incluindo a descrição de estruturas lineares e em árvore, como XML, JSON, hierarquias de exibição etc.


Como trabalhar com isso?


Você pode criar seu próprio construtor de funções, foi mais fácil para mim entender como funciona. Considere um exemplo primitivo de um construtor que concatena cadeias.


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

Sob o capô, isso funcionará assim:


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

É importante que, na implementação do construtor, os métodos sejam exatamente estáticos, com nomes específicos e com um tipo específico de parâmetros nessa lista . Você pode alterar apenas o tipo e o nome do parâmetro de entrada.


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

Nomes de métodos específicos serão pesquisados ​​no construtor e substituídos no estágio de compilação. E se o método não for encontrado, ocorrerá um erro de compilação.
E isso é mágico. Quando você implementa o construtor, o compilador não informa nada. Ele não informará sobre os métodos disponíveis, não ajudará no preenchimento automático. Somente quando você escreve um código do cliente que não pode ser processado por esse construtor, você recebe um erro indistinto.
Até agora, a única solução que encontrei é ser guiada por uma lista de métodos .
Então, por que precisamos de outros métodos? Bem, por exemplo, para suportar esse código com verificações


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

Para suportar esta sintaxe, o construtor precisa implementar os buildEither(first:/second:)


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

Resposta da comunidade


O engraçado é que isso ainda não está no Swift 5.1, ou seja, uma solicitação de recebimento com esse recurso ainda não foi enviada, mas, no entanto, a Apple já o adicionou ao Xcode 11 beta. E em Criadores de funções → Lançamentos → Fóruns rápidos, você pode ver a reação da comunidade a essa proposta.


ViewBuilder


Agora volte ao VStack e veja a documentação de seu inicializador init (alignment: spacing: content :) .
É assim:


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

E antes da tabela de conteúdo, existe um atributo personalizado @ViewBuilder
É declarado da seguinte forma:


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

O atributo de usuário é @ _functionBuilder , registrado no início de sua declaração.


E se você examinar a documentação ainda mais, poderá ver muitos métodos estáticos de buildBlock que diferem no número de argumentos.


Isso significa que o código de exibição


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

sob o capô é convertido em tal


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

I.e. cumpre o método do construtor buildBlock (:: _ :) .


Da lista inteira, o método com o número máximo de argumentos é esse cara buildBlock (:::::::: :) :) (10 argumentos):


 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 } 

E, voltando ao exemplo original, quando você tenta aumentar o VStack e onze visualizações, o compilador tenta encontrar o método ViewBuilder'a buildBlock , que possui 11 argumentos de entrada. Mas não existe esse método: daí o erro de compilação.
Isso é verdadeiro para todas as coleções que usam a inicialização com o atributo @ViewBuilder no inicializador: V | H | Z-Stack, List, Group e outros, dentro dos quais você pode declarar mais de uma exibição como uma enumeração.
E isso é triste.


MEM (desculpe, não encontrei um meme decente)


Como ser


Podemos contornar essa limitação usando o 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 } } 

Ou aninhando coleções:


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

Mas essas decisões parecem muletas e há apenas esperança para um futuro melhor. Mas como é esse futuro?
Swift já possui parâmetros Variadic . Essa é a capacidade de um método para aceitar argumentos por enumeração. Por exemplo, o método de print conhecido por todos permite que você escreva print(1, 2) e print(1, 2, 3, 4) e isso sem sobrecargas desnecessárias do método.


 print(items: Any...) 

Mas esse recurso da linguagem não é suficiente, pois o método buildBlock aceita diferentes argumentos genéricos como entrada.
Adicionar genéricos Variadic resolveria esse problema. Os genéricos variáveis ​​permitem que você abstraia de muitos tipos genéricos, por exemplo, algo como isto:


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

E a Apple é simplesmente obrigada a adicionar isso. Tudo repousa contra esse mecanismo agora. E parece-me que eles simplesmente não conseguiram terminá-lo até a WWDC 2019 (mas isso é apenas especulação).

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


All Articles