SwiftUI defaults considerados prejudiciais
Alguns comentários sobre SwiftUI e por que os frameworks de interface do usuário não devem tentar ser "inteligentes".
A abordagem geral que SwiftUI está adotando (quadro reativo, declarativo baseado em dados para a interface do usuário) é realmente sólida e considerada estado-da-arte no dia atual. Sem reclamações aqui, ótimo trabalho, todos precisávamos disso, obrigado Apple por lançar isso. Não, sério. É uma ferramenta incrível e estou ansioso para usá-la.
No entanto, algumas coisas que notei em SwiftUI me preocupam. Acho que eles poderiam ilustrar pontos na concepção de frameworks de interface do usuário que sistemas futuros poderiam lidar melhor. Sem mais delongas, vamos começar com o maior problema no design da API: vírgulas!
Guerra contra vírgulas
Estou muito impressionado com até onde as pessoas estão dispostas a ir para alcançar um DSL realmente elegante. Quero dizer, SwiftUI poderia ter sido:
VStack(
Image(uiImage: image),
Text(title),
Text(subtitle)
)
mas em vez disso, eles decidiram eliminar essas irritantes vírgulas entre os elementos. Para fazer isso, alteraram a linguagem para que lambdas pudessem ter múltiplos retornos implícitos. Como este:
VStack {
Image(uiImage: image)
Text(title)
Text(subtitle)
}
Quero dizer, a única diferença antes e depois é se você tem que colocar uma vírgula entre os elementos ou não. E confie em mim, fazer isso acontecer não foi fácil. Esta é uma solução que Apple criou:
static func buildBlock() -> EmptyView
static func buildBlock<Content>(Content) -> Content
static func buildBlock<C0, C1>(C0, C1) -> TupleView<(C0, C1)>
static func buildBlock<C0, C1, C2>(C0, C1, C2) -> TupleView<(C0, C1, C2)>
static func buildBlock<C0, C1, C2, C3>(C0, C1, C2, C3) -> TupleView<(C0, C1, C2, C3)>
static func buildBlock<C0, C1, C2, C3, C4>(C0, C1, C2, C3, C4) -> TupleView<(C0, C1, C2, C3, C4)>
static func buildBlock<C0, C1, C2, C3, C4, C5>(C0, C1, C2, C3, C4, C5) -> TupleView<(C0, C1, C2, C3, C4, C5)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(C0, C1, C2, C3, C4, C5, C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(C0, C1, C2, C3, C4, C5, C6, C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(C0, C1, C2, C3, C4, C5, C6, C7, C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)>
Espero que você tenha uma tela grande o suficiente para ler isso. Não só isso parece improvisado e sem graça, mas também não permite colocar mais de 10 elementos em um contêiner! Tudo porque alguém responsável pelo design da API estava com medo de listas e tinha mais poder do que alguém responsável pela linguagem Swift.
Estes três métodos parecem preocupantes também:
static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent>
static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent>
static func buildIf<Content>(Content?) -> Content?
Não porque você não precisa de if — é claro que você precisa disso — mas o que acontece com toda outra construção de linguagem? Posso usar essas dentro dessas lambdas especiais sem suporte especial? Quero dizer, for é suportado separadamente. Mas e o que acontece com while, repeat, switch, continue, break, throw e outros então? Não é o ponto de programar em uma linguagem poder usar essa linguagem?
Envoltórios implícitos
A concepção de componentes levanta algumas perguntas também. E.g. em
VStack {
Text("abc")
.bold()
.padding(.all)
}
.bold() altera o texto, mas .padding() envolve-o em outra view, mudando o tipo de retorno da expressão inteira. Compare isso com VStack, que envolve explicitamente seus filhos. Por que fazer essa distinção? Se você sentar para projetar um DSL elegante, não deveria também transmitir algum significado? Um pouco? Para ajudar o leitor a entender o código mais rapidamente? Por que esconder views de envoltório dentro de cadeias de chamadas de método se já estabeleceu outra maneira visual de envolver views?
VStack {
Padding {
Text("abc").bold()
}
}
Invasão de privacidade do filho
Alguns itens provavelmente são apenas erros (muito engraçado). Por exemplo, NavigationView pega suas propriedades não do construtor ou por meio de modificadores, mas sim das propriedades de seu primeiro filho. POR QUE?
NavigationView {
List {...}
.navigationBarTitle(Text("Rooms"))
}
Defaultes inteligentes
Tudo bem, mas essas pequenas irritações. Não entendo por que existem, mas sua existência não traz nada horrível também. Diferente dos defaultes do SwiftUI:
Text("abc").padding()
Isto envolverá o texto com um padding de... eu não sei! Ninguém sabe, exatamente. O SwiftUI decide qual será esse padding, de acordo com algum raciocínio interno. Talvez seja algum valor codificado (que bom que espero!). Pode ser vários valores, dependendo do dispositivo. Ou da orientação. Ou do tamanho da tela. Ou idioma, ciclo dia/noite, fase lunar - meu ponto é, ninguém sabe ou pode saber com certeza.
Tudo bem, mas se você vê padding sem argumentos, pode supor que algo estranho está acontecendo. E como sobre isso?
HStack {
Text("★★★★★")
Text("Avocado Toast).font(.title)
...
}
Vê o padding aqui? Não vê? Mas olhe na imagem!
Deixo uma citação de David Abrahams:
O SwiftUI não empurrou todos os filhos da pilha um contra o outro. Ele deixou algum espaço entre esses dois porque Adaptive Spacing™ está em vigor.
Então o SwiftUI decidiu que, mesmo que nós não tivéssemos pedido isso, seria bom ter algum espaço entre esses dois elementos. Outro default, você diria. Bem, não exatamente. Como eu entendo, o SwiftUI irá olhar dentro da hierarquia de views e se reconhecer algumas das views pode fazer chamadas bastante complexas sobre quanto espaço deveria adicionar. Por exemplo, se tiver Text em algum lugar profundo no seu componente, ele pode usar sua linha de base ao invés do retângulo contêiner:
Outro exemplo de comportamento “inteligente” (ou “mágico”). Um botão pode parecer preto se você adicioná-lo a uma lista:
List {
Button { Text("Add room") }
}
mas azul se você mudar listStyle (não o estilo do botão! você não precisa tocar no botão em absoluto!):
List {
Button { Text("Add room") }
}.listStyle(.grouped)
E aqui está minha preocupação. Como uma pessoa com experiência extensa em programação web, que começou quando a web nem era divertida nem bonita, tenho dúvidas particulares sobre regras mágicas inteligentes gentilmente fornecidas pela plataforma.
Primeiro, às vezes os defaultes apenas ficam no caminho. Isso significa que você precisa desfazê-los. Não pode ser tão fácil quanto parece. Claro, mudar padding é fácil se você vê isso em seu código:
Text().padding() -> Text().padding(5)
Mas e se não houver nada? Nenhum código? Como desfazer isso?
HStack { Text() Text() Text() } -> ?
Quando corrigindo um layout quebrado, é sempre mais fácil adicionar coisas que você esqueceu do que remover coisas que o framework fez por você e que você não pode ver. Com código escrito, você pode encontrá-lo, lê-lo, entendê-lo, depurá-lo, alterá-lo. O código de um framework é completamente opaco para você.
Em segundo lugar, comportamentos inteligentes podem ser um pesadelo quando eles não funcionam do seu jeito. Na web, muitas pessoas perderam milhões de horas no StackOverflow tentando descobrir como remover espaço extra ao redor de img, alinhar ícone com texto em um botão, se livrar de uma rolagem desnecessária, desfazer o aumento de texto para dispositivos móveis, remover atraso de toque em links, fazer floats alinharem corretamente, corrigir espaçamento ao redor de elementos inline, etc. Todos esses problemas surgiram porque o HTML tem componentes aparentemente simples que encapsulam comportamentos muito complexos. Às vezes ele faz exatamente o que você precisa e você está feliz, mas às vezes não faz e você não sabe o que fazer.

Citando Rob Napier:
Ao meu ver, o mais difícil sobre SwiftUI é que ele é muito não descobrível e a documentação é incrivelmente escassa, então você só pode realmente saber isso ao se aprofundar no sistema bastante (isso é bem fácil, mas alinhamentos são criaturas incrivelmente sutis). De muitas maneiras, é bastante como CSS nesse sentido. Há muitos knobs para ajustar e não é óbvio onde você procuraria por eles, novamente como CSS.
O terceiro problema com os padrões é que às vezes eles criam variações das quais você simplesmente não está ciente. Você pode estar feliz com seu layout em um simulador, mas em algum lugar em alguma versão estranha de iPad em uma determinada orientação SwiftUI gentilmente define o padding para um valor maior e quebra seu layout. Viola! Na web, costumávamos ter CSS reset exatamente por essa razão: você nunca pode estar seguro.
A quarta questão é que mesmo se você tiver criado um aplicativo perfeito e testado extensivamente em todas as variações possíveis, quem garante que amanhã a Apple não ficará entediada com o design linguagem atual e lançará o SwiftUI 5.6.7 com padrões completamente diferentes? Ou, pior ainda, com ligeiramente diferentes padrões?
A solução
...é ser bobo e explícito! Um framework deve fazer o que lhe foi dito e não fazer o que não lhe foi. Quanto mais simples, melhor. Se eu esqueci de colocar padding entre os elementos HStack, bem, vergonha para mim, não deveria haver padding! Todos os erros são meus.
Claro, algumas nuances podem não ser tão boas quanto deveriam nas mãos de um desenvolvedor inexperiente. Essa é a lacuna que pode ser coberta com aprendizado. Diferentemente da situação atual, onde as coisas parecem ótimas no início, mas se tornam uma pesadelo de manutenção mais tarde. Em ferramentas profissionais, a previsibilidade supera a conveniência para iniciantes. Quero que mais frameworks sigam isso.

