SwiftUI nas prateleiras

Toda vez que uma nova estrutura aparece em uma linguagem de programação, mais cedo ou mais tarde, aparecem pessoas que aprendem a linguagem com ela. Provavelmente esse foi o caso no desenvolvimento do IOS no momento do surgimento do Swift: no início, era considerado um complemento ao Objective-C - mas ainda não o encontrei. Agora, se você começar do zero, a escolha do idioma não vale mais a pena. Swift está além da concorrência.

A mesma coisa, mas em menor escala, está acontecendo com estruturas. A aparência do SwiftUI não é exceção. Provavelmente sou um representante da primeira geração de desenvolvedores que começou aprendendo SwiftUI, ignorando o UIKit. Isso tem um preço - até o momento existem muito poucos materiais de treinamento e exemplos de código de trabalho. Sim, a rede já possui vários artigos informando sobre um recurso específico, uma ferramenta específica. No mesmo site www.hackingwithswift.com , já existem muitos exemplos de código com explicações. No entanto, eles fazem pouco para ajudar aqueles que decidem aprender SwiftUI do zero, como eu. A maioria dos materiais da rede são respostas a perguntas específicas e formuladas. Um desenvolvedor experiente pode descobrir facilmente como tudo funciona, por que é assim e por que deve ser aplicado. Para um iniciante, primeiro você precisa entender que pergunta fazer e só então ele poderá acessar esses artigos.



De acordo com o corte, tentarei sistematizar e classificar o que consegui aprender no momento. O formato do artigo é quase um guia, embora seja uma folha de dicas compilada por mim na forma em que eu mesmo gostaria de lê-lo no início da minha jornada. Para desenvolvedores experientes que ainda não se aprofundaram no SwiftUI, também existem alguns exemplos de código interessantes e explicações em texto podem ser lidas na diagonal.

Espero que este artigo poupe algum tempo em que você também queira sentir um pouco de mágica.

Para começar, um pouco sobre você


Praticamente não tenho experiência em desenvolvimento móvel e experiência significativa
em 1s não poderia ajudar muito aqui. Sobre como e por que decidi aprender o SwiftUI, vou lhe dizer outra hora, se for interessante para alguém, é claro.

Aconteceu que o início da minha imersão no desenvolvimento móvel coincidiu com o lançamento do iOS 13 e SwiftUI. Este é um sinal, pensei, e decidi começar imediatamente com ele, ignorando o UIKit. Achei uma coincidência divertida ter começado a trabalhar com o 1c nesses momentos: então apenas os formulários gerenciados apareceram. No caso de 1c, a popularização de novas tecnologias levou quase cinco anos. Toda vez que um desenvolvedor era instruído a implementar algumas novas funcionalidades, ele enfrentava uma opção: fazê-lo de maneira rápida e confiável, com ferramentas familiares, ou gastar muito tempo mexendo em novas, e sem garantias de resultados. No momento, a escolha era feita em favor da velocidade e da qualidade, e o tempo de investimento em novas ferramentas foi adiado por um período muito longo.

Agora, aparentemente, a situação com o SwiftUI é praticamente a mesma. Todo mundo está interessado, todo mundo entende que esse é o futuro, mas até agora poucos se dedicaram a estudá-lo. A menos que para projetos de animais de estimação.

Em geral, não me importava qual estrutura estudar, e decidi me arriscar, apesar da opinião geral de que seria possível lançá-la em produção em um ano ou dois. E como eu estava entre os pioneiros, decidi compartilhar experiências práticas. Quero dizer que não sou um guru e, em geral, no desenvolvimento móvel - uma chaleira. No entanto, eu já segui um certo caminho, durante o qual pesquisei toda a Internet em busca de informações, e posso dizer com confiança que isso não é suficiente e praticamente não é sistematizado. Mas, em russo, é claro, é praticamente inexistente. Nesse caso, decidi reunir minhas forças, afastar o complexo do impostor e compartilhar com a comunidade o que consegui descobrir. Partirei da suposição de que o leitor já está pelo menos minimamente familiarizado com o SwiftUI e não decifrarei coisas como VStack{…} , Text(…) etc.

Enfatizo mais uma vez que descreverei melhor minhas próprias impressões de tentativas para alcançar o resultado desejado do SwiftUI. Eu não conseguia entender algo e, a partir de algumas experiências, para tirar conclusões erradas ou imprecisas, portanto quaisquer correções e esclarecimentos são fortemente bem-vindos.

Para desenvolvedores experientes, este artigo pode parecer cheio de descrições de coisas óbvias, mas não julga rigorosamente. Tutoriais para manequins no SwiftUI ainda não foram escritos.

Que diabos é esse SwiftUI?


Então, eu provavelmente começaria com o que se trata, este é o seu SwiftUI. Aqui, novamente, meu primeiro passado surge. A analogia com os formulários gerenciados só ficou mais forte quando assisti a alguns vídeos tutoriais sobre como fazer o layout de interfaces no Storyboard (por exemplo, ao trabalhar com o UIKit). Tomei nostalgia de acordo com as formas "incontroláveis" em 1s: colocação manual de elementos no formulário e, especialmente, ligações ... Ah, quando o autor do vídeo de treinamento por cerca de 20 minutos falou sobre os meandros da ligação de vários elementos entre si e as bordas da tela, lembrei com um sorriso 1C - tudo era o mesmo antes de formas controladas. Bem, quase ... um pouco pior, é claro, bem, e consequentemente - mais fácil. E o SwiftUI é, aproximadamente, formulários gerenciados da Apple. Sem ligações. Não há storyboards e segways. Você simplesmente descreve a estrutura da sua exibição no código. E isso é tudo. Todos os parâmetros, tamanhos, etc. são definidos diretamente no código - mas de maneira bastante simples. Mais precisamente, você pode editar os parâmetros dos objetos existentes no Canvas, mas, para isso, primeiro é necessário adicioná-los ao código. Honestamente, não sei como isso funcionará em grandes equipes de desenvolvimento, onde é habitual separar o layout do design e o conteúdo do próprio View, mas como desenvolvedor independente, gosto muito dessa abordagem.

Estilo declarativo


O SwiftUI pressupõe que a descrição da estrutura da sua visualização esteja totalmente em código. Além disso, a Apple nos oferece um estilo declarativo de escrever esse código. Ou seja, algo como isto:
“Esta é uma visão. (Por algum motivo, quero dizer "visualizar" e, portanto, aplicar declinação a uma palavra feminina) consiste em dois campos de texto e uma imagem. Os campos de texto são organizados um após o outro horizontalmente. A imagem está embaixo deles e suas bordas são cortadas na forma de um círculo ".
Parece incomum, certo? Normalmente, no código que descrevemos o processo em si, o que precisa ser feito para alcançar o resultado que temos em nossa mente:
“Insira um bloco, insira um campo de texto nesse bloco, seguido por outro campo de texto e, depois disso, tire uma foto, corte as bordas arredondando-as e cole abaixo.”
Parece uma instrução para móveis da Ikea. E no swiftUI, vemos imediatamente qual deve ser o resultado. Mesmo sem o Canvas ou a depuração, a estrutura do código reflete claramente a estrutura da Visualização. É claro o que e em que sequência será exibida e com quais efeitos.

Um excelente artigo sobre o FunctionBuilder e como ele permite escrever código em um estilo declarativo já está em Habré .

Em princípio, muito foi escrito sobre o estilo declarativo e suas vantagens, então eu o arredondarei. Acrescentarei que me acostumei e realmente senti como é conveniente escrever código nesse estilo quando se trata de interfaces. Com isso, a Apple atingiu o que é chamado de alvo!

Em que consiste o View?


Mas vamos dar uma olhada. A Apple sugere que o estilo declarativo é assim:

 struct ContentView: View { var text1 = "some text" var text2 = "some more text" var body: some View { VStack{ Text(text1) .padding() .frame(width: 100, height: 50) Text(text2) .background(Color.gray) .border(Color.green) } } } 

Observe que o View é uma estrutura com alguns parâmetros. Para criar a estrutura View , precisamos definir o body parâmetro calculado, que retorna some View . Falaremos sobre isso mais tarde. O conteúdo do body: some View { … } fechamento do body: some View { … } é a descrição do que será exibido na tela. Na verdade, é tudo o que é necessário para nossa estrutura atender aos requisitos do protocolo View. Sugiro focar principalmente no body .

E assim, prateleiras


No total, contei três tipos de elementos a partir dos quais o corpo do View é construído:

  • Outra vista
    I.e. Cada Visualização contém uma ou mais outras. Esses, por sua vez, também podem conter tanto a View predefinida do sistema como Text() quanto as personalizadas e complexas, criadas pelo desenvolvedor. Acontece um tipo de boneca com um nível ilimitado de aninhamento.
  • Modificadores
    Com a ajuda de modificadores, toda mágica acontece. Graças a eles, informamos de forma breve e clara ao SwiftUI que tipo de visão queremos ver. Como funciona, ainda vamos descobrir, mas o principal é que os modificadores adicionam a peça necessária ao conteúdo de uma determinada View .
  • Contentores
    Os primeiros contêineres com os quais o padrão "Olá, mundo" começa são o HStack e o VStack . Um pouco mais tarde, Group , Section e outros aparecem. De fato, os contêineres são a mesma visualização, mas eles têm um recurso. Você transmite a eles algum conteúdo que deseja exibir. O recurso completo do contêiner é que ele deve, de alguma forma, agrupar e exibir os elementos desse conteúdo. Nesse sentido, os contêineres são semelhantes aos modificadores, com a única diferença de que os modificadores pretendem alterar uma Visualização pronta e os contêineres organizam essa Visualização (elementos de conteúdo ou blocos de sintaxe declarativos) em uma determinada ordem, por exemplo, vertical ou horizontalmente ( VStack{...} HStack{...} ). Também existem contêineres específicos, como ForEach ou GeometryReader , falaremos sobre eles um pouco mais tarde.

    Em geral, considero os contêineres como qualquer Visualização, para a qual o Conteúdo pode ser passado como parâmetro.

E isso é tudo. Todos os elementos do SwiftUI de raça pura podem ser atribuídos a um desses tipos. Sim, isso não é suficiente para preencher a sua visualização com funcionalidade, mas é tudo o que você precisa para mostrar sua funcionalidade na tela.

.modifiers () - como eles são organizados?


Vamos começar com o mais simples. Um modificador é realmente uma coisa muito simples. Ele apenas pega alguma View , aplica algumas alterações nela (ou faz?) , E devolve. I.e. O modificador é uma função da própria View , que retorna a self , tendo realizado anteriormente algumas modificações.

Abaixo está um exemplo de código com o qual declaro meu próprio modificador. Mais precisamente, sobrecarrego o frame(width:height:) modificador existente frame(width:height:) , com o qual você pode corrigir as dimensões específicas de uma Visualização específica. Na caixa, você precisa especificar a largura e a altura para isso, e eu precisava passar um objeto CGSize para ele com um argumento, que é uma descrição apenas do comprimento e largura. Por que eu preciso disso, vou contar um pouco mais tarde.

 struct FrameFromSize: ViewModifier{ let size: CGSize func body(content: Content) -> some View { content .frame(width: size.width, height: size.height) } } 

Com esse código, criamos uma estrutura que está em conformidade com o protocolo ViewModifier . Esse protocolo exige que a função body() seja implementada nessa estrutura, cuja entrada será um pouco de Content e a saída terá some View : o mesmo tipo que o parâmetro body da nossa View (falaremos sobre alguma view abaixo) . Que tipo de Content esse?

Content + ViewBuilder = Visualizar


Na documentação interna sobre ele, é o que diz:
`content` é um proxy para a visualização que terá o modificador representado por` Self` aplicado a ela.
Este é um tipo de proxy, que é uma pré-fabricada View à qual os modificadores podem ser aplicados. Um tipo de produto semi-acabado. Na verdade, o Content é um fechamento de estilo declarativo que descreve a estrutura de View . Assim, se chamamos esse modificador de alguma Visão, tudo o que ele faz é obter um fechamento do body e passá-lo para a função do nosso body , na qual adicionamos nossos cinco centavos a esse fechamento.

Mais uma vez, o View é principalmente uma estrutura que armazena todos os parâmetros necessários para gerar uma imagem na tela. Incluindo instruções de montagem, das quais o Content . Assim, um fechamento em um estilo declarativo ( Content ) processado usando o ViewBuilder retorna uma View.

Vamos voltar ao nosso modificador. Em princípio, a declaração da estrutura FrameFromSizeFrameFromSize suficiente para começar a aplicá-la. Dentro do body podemos escrever assim:

 RoundedRectangle(cornerRadius: 4).modifier(FrameFromSize(size: size)) 

modifier é um método de protocolo de exibição que extrai o conteúdo da View modificada, passa-o para a função do corpo da estrutura do modificador e passa o resultado para o processamento do ViewBuilder ou para o próximo modificador, se houver uma cadeia de modificações.

Mas você pode torná-lo ainda mais conciso, declarando seu próprio modificador como uma função, expandindo os recursos do protocolo View.

 extension View{ func frame(_ size: CGSize) -> some View { self.modifier(FrameFromSize(size: size)) } } 

Nesse caso, sobrecarreguei o modificador existente .frame(width: height:) outra variante dos parâmetros de .frame(width: height:) . Agora, podemos usar a opção de chamar o modificador de frame(size:) para qualquer View . Como se viu, nada complicado.

Um pouco sobre erros
A propósito, eu pensei que não era necessário expandir todo o protocolo, seria suficiente expandir especificamente o RoundedRectangle no meu caso, e deveria ter funcionado, como me pareceu - mas parece que o Xcode não esperava tal imprudência e caiu com um erro ininteligível “ Abort trap: 6 "e uma proposta para enviar um despejo aos desenvolvedores. De um modo geral, no SwiftUI, as descrições de erros, até o momento, muitas vezes não revelam completamente a causa desse erro.

Da mesma maneira, você pode criar quaisquer modificadores personalizados e usá-los da mesma maneira que o SwiftUI embutido:

 RoundedRectangle(cornerRadius: 4).frame(size) 

Convenientemente, concisa, claramente.

Imagino uma cadeia de modificações como contas amarradas em um fio - nossa Visão. Essa analogia também é verdadeira no sentido de que a ordem na qual as modificações são chamadas é importante.



Quase tudo no SwiftUI é View
A propósito, uma observação interessante. Como parâmetro de entrada, o plano de fundo não aceita cores, mas Visualização. I.e. A classe Color não é apenas uma descrição da cor, é uma Visualização completa, na qual modificadores e muito mais podem ser aplicados. E como pano de fundo, consequentemente, você pode passar para outra visualização.

Modificadores - apenas para modificações
Talvez valha a pena notar mais um ponto. Modificadores que não alteram o conteúdo de origem são simplesmente ignorados pelo SwiftUI e não são chamados. I.e. Você não poderá disparar com base no modificador que causa alguns eventos, mas não executa nenhuma ação com o conteúdo. A Apple persistentemente nos leva a abandonar algumas ações em tempo de execução ao renderizar a interface e confiar no estilo declarativo.

Visualização estática


Anteriormente, falamos sobre o que o body consiste, o corpo da View ou suas instruções de montagem. Vamos voltar ao próprio modo de View . Antes de tudo, é uma estrutura na qual alguns parâmetros podem ser declarados, e body é apenas um deles. Como já dissemos, descobrindo o que Content , body é uma instrução sobre como montar a Visualização desejada, que é um fechamento em estilo declarativo. Mas o que deve retornar nosso fechamento?

alguma visão - conveniência




E chegamos a uma pergunta tranqüila que, durante muito tempo, eu não conseguia entender, embora isso não me impedisse de escrever código de trabalho. O que é isso? A documentação diz que esta descrição é um "tipo de resultado opaco" - mas isso não faz muito sentido.

A palavra-chave some é uma versão "genérica" ​​de uma descrição de um tipo retornado por um fechamento que não depende de nada além do próprio código. I.e. O resultado do acesso à propriedade calculada do corpo da nossa View deve ser alguma estrutura que satisfaça o protocolo View. Pode haver muitos deles - Texto, Imagem ou talvez alguma estrutura que você declarou. O chip inteiro da palavra-chave some é declarar "genérico" que está em conformidade com o protocolo View. Ele é estaticamente determinado pelo código implementado no corpo da sua Visualização, e o Xcode é capaz de analisar esse código e calcular a assinatura específica do valor de retorno (bom, quase sempre) . E algumas são apenas uma tentativa de não sobrecarregar o desenvolvedor com cerimônias desnecessárias. É suficiente para o desenvolvedor dizer: "haverá algum tipo de visualização" e qual - resolva você mesmo. A chave aqui é que o tipo concreto é determinado não pelos parâmetros de entrada, como no tipo genérico usual, mas diretamente pelo código. Portanto, acima, genérico eu citei.

O Xcode deve ser capaz de identificar um tipo específico sem saber exatamente quais valores você passa para essa estrutura. É importante entender: após a compilação, a expressão Some View é substituída pelo tipo específico de sua View . Esse tipo é bastante determinístico e pode ser bastante complexo, por exemplo, assim: Group<TupleView<(Text, ForEach<[SomeClass], SomeClass.ID, Text>)>> .

O código de amostra pode ser restaurado deste tipo:

 Group{ Text(…) ForEach(…){(value: SomeClass) in Text(…) } } 

ForEach , como pode ser visto na assinatura de tipo, não é um loop de tempo de execução. Esta é apenas uma View que é construída com base em uma matriz de objetos SomeClass . como identificador de um subView específico associado ao elemento de coleção, o ID do elemento é indicado e, para cada elemento, um subView tipo Text é subView . Text e ForEach combinados no TupleView , e tudo isso é colocado em Group . Falaremos mais sobre o ForEach mais detalhes.

Imagine quanto seria a escrita se fôssemos forçados a descrever uma assinatura exata como o body do parâmetro? Para evitar isso, a palavra-chave some foi criada.

Sumário
some são genéricos - vice-versa. Obtemos o genérico clássico de fora da função e, já sabendo o tipo específico do tipo genérico, o Xcode define como nossa função funciona. some- não depende dos parâmetros de entrada, mas apenas do próprio código. Isso é simplesmente uma abreviação, que permite não definir um tipo específico, mas indicar apenas a família do valor retornado pela função (protocolo).

alguma visão - e consequências


A abordagem para calcular o tipo estático de uma expressão dentro do corpo gera, na minha opinião, dois pontos importantes:

  • Quando compilado, o Xcode analisa o conteúdo do corpo para calcular o tipo de retorno específico. Em corpos complexos, isso pode levar algum tempo. Em alguns corpos particularmente complexos, ele pode não ser capaz de lidar com o tempo são, e o dirá diretamente.

    Em geral, o View precisa ser mantido o mais simples possível. Estruturas complexas são melhor colocadas em modo de exibição separado. Assim, cadeias inteiras de tipos reais são substituídas por um tipo - seu CustomView, que permite ao compilador não enlouquecer com toda essa bagunça.
    A propósito, é realmente muito conveniente depurar um pequeno pedaço de uma visualização grande, bem aqui, em tempo real, recebendo e observando o resultado no Canvas.
  • Não podemos controlar diretamente o fluxo. Se o If - else, o SwiftUI ainda puder processá-lo, criando uma "Schrödinger View" do tipo <_ConditionalContent <Text, TextField >>, o operador da condição de trinar poderá ser usado apenas para selecionar um valor de parâmetro específico, mas não um tipo, ou mesmo para selecionar uma sequência de modificadores.

    Mas vale a pena restaurar a mesma ordem de modificadores, e esse registro deixa de ser um problema.

Exceto corpo


No entanto, pode haver outros parâmetros na estrutura com os quais você pode trabalhar. Como parâmetros, podemos declarar o seguinte.

Parâmetros externos


Estes são parâmetros de estrutura simples que devemos passar de fora durante a inicialização para que o View os processe de alguma forma:

 struct TextView: View { let textValue: String var body: some View { Text(textValue) } } 

Neste exemplo, o textValue para a estrutura TextView é um parâmetro que deve ser preenchido externamente porque não possui um valor padrão. Dado que as estruturas suportam a geração automática de inicializadores, podemos usar esta Visualização simplesmente:

  TextView(textValue: "some text") 

Do lado de fora, você também pode transferir fechamentos que precisam ser executados quando um evento ocorre. Por exemplo, Button(lable:action:) faz exatamente isso: executa o fechamento da ação passada quando um botão é clicado.

estado - parâmetros


O SwiftUI está usando ativamente o novo recurso do Swift 5.1 - Property Wrapper .

Antes de tudo, são variáveis ​​de estado - parâmetros armazenados de nossa estrutura, cuja mudança deve ser refletida na tela.Eles são embrulhados em invólucros especiais @State- para tipos primitivos e @ObservedObject- para classes. Uma classe deve satisfazer o protocolo ObservableObject- isso significa que essa classe deve poder notificar os assinantes (View, que usam esse valor com um wrapper @ObservedObject) sobre a alteração em suas propriedades. Para fazer isso, basta agrupar as propriedades necessárias @Published.

Se você não estiver procurando maneiras fáceis ou precisar de funcionalidade adicional, em vez deste wrapper, poderá usar ObservableObjectPublishere enviar notificações manualmente usando os eventos willSet()desses parâmetros, conforme descrito, por exemplo, aqui .

Lembre-se, eu disse issobodyIsso é apenas uma propriedade computável? No começo, não entendi imediatamente tudo sobre as variáveis ​​de estado e tentei declarar algumas variáveis ​​de estado dentro bodysem invólucros. O problema acabou sendo que body, como eu disse, é uma instrução sem estado. A visão foi gerada de acordo com esta instrução, e todo o contexto declarado dentro do corpo foi para aterro. Em seguida, apenas os parâmetros de estrutura armazenados são ativados. Ao alterar os parâmetros de estado, todos os nossos são Viewatualizados. A instrução é novamente tomada, os valores atuais de todos os parâmetros da estrutura são substituídos, a imagem na tela é coletada e a instrução é lançada novamente até a próxima vez. Variáveis ​​declaradas dentro body- junto com ele. Para desenvolvedores experientes, isso pode ser óbvio, mas a princípio fiquei atormentado com isso, sem entender a essência do processo.

E mais uma observação
Você não pode usar didSet willSeteventos de parâmetros de estrutura agrupados em nenhum invólucro. O compilador permite que você escreva esse código, mas ele simplesmente não é executado. Provavelmente porque o wrapper é algum tipo de código de modelo que é executado quando esses eventos ocorrem.

Exemplo de estado clássico :

 struct ContentView: View { @State var tapCount = 0 var body: some View { VStack { Button(action: {self.tapCount += 1}, label: { Text("Tap count \(tapCount)") }) } } } 

Parâmetros de ligação


Bem, para refletir algumas alterações no serviço de exibição @State @ObservedObject. Mas como essas mudanças são passadas View? Para fazer isso, o SwiftUI possui outro PropertyWrapper - @Binding. Vamos complicar nosso exemplo com um botão para contar cliques. Suponha que tenhamos um pai View, que reflete, entre outras coisas, um contador de cliques e um filho Viewcom um botão. Na visualização principal, o contador é declarado como @State- é compreensível, mas queremos que o contador na tela seja atualizado. Mas na subsidiária, o contador deve ser declarado como @Binding. Este é outro Property Wrapper, com o qual declaramos parâmetros de estrutura que não apenas mudam, mas também retornam ao pai View. Este é um tipo de inoutmarcador paraView. Quando um valor é alterado em uma exibição filho, essa alteração é convertida de volta para a exibição pai, de onde veio originalmente. E, assim como inout, precisamos marcar os valores transmitidos com um símbolo especial $,para mostrar que estamos esperando o valor transmitido mudar dentro de outra visualização. Reaja em ação.

 struct ContentView: View { @State var tapCount = 0 var body: some View { VStack{ SomeView(count: $tapCount) Text("you tap \(tapCount) times") } } } 

Isso também se reflete nos tipos de dados. @Binding var tapCount: Intpor exemplo, não é mais apenas um Inttipo, é

 Binding<Int> 

Isso é útil para saber, por exemplo, se você deseja escrever seu próprio inicializador View.

 struct SomeView: View{ @Binding var tapCount: Int init(count: Binding<Int>){ self._tapCount = count //    -    } var body: some View{ Button(action: {self.tapCount += 1}, label: { Text("Tap me") }) } } 

Observe que dentro de initvocê @PropertyWrapperdeve usar um sublinhado para se referir aos parâmetros agrupados em alguns parâmetros self._- isso funciona nos inicializadores, quando selfainda está em processo de criação. Mais precisamente, com a ajuda, self._nos referimos ao parâmetro junto com seu invólucro. A aplicação direta ao valor dentro do wrapper é feita sem sublinhado.

Por sua vez, se você tiver uma variável agrupada em algum tipo de entrada PropertyWrapper, obteremos um tipo de wrapper, nesse caso

 Binding<Int> 

Você Intpode acessar diretamente o valor do tipo .wrappedValue.

E como sempre, um rake pessoal
, Binding . View View. View , @Binding-. , View State @Binding — , State- Binding. -, , .

EnvironmentObject


Em resumo, os EnvironmentObjectparâmetros são como Binding, apenas imediatamente para todos Viewna hierarquia, sem a necessidade de passá-los explicitamente.

 ContentView().environmentObject(session) 

Geralmente, o estado atual do aplicativo, ou parte dele, que muitos View precisam de uma vez, é transmitido. Por exemplo, dados sobre um usuário, sessão ou algo semelhante, faz sentido colocá-lo no EnvironmentObject uma vez, na Visualização raiz. Em cada Visualização, onde são necessárias, elas podem ser extraídas do ambiente, declarando uma variável com um wrapper @EnvironmentObject, por exemplo, como este

  @EnvironmentObject var session: Session 

O identificador de um valor específico é o próprio tipo. Se você inserir EnvironmentObjectvários valores do mesmo tipo, o pedido será importante. Para chegar ao terceiro, por exemplo, valor, você precisa obter todos os valores em ordem, mesmo que não precise deles. Portanto, EnvironmentObjecté adequado para refletir o estado do aplicativo, mas não é adequado para passar vários valores do mesmo tipo entre eles View. Eles terão que ser transmitidos manualmente, através Binding.

@ Environment é quase o mesmo. No sentido, este é um estado do ambiente, ou seja, OS É conveniente passar por esse invólucro, por exemplo, a posição da tela (vertical ou horizontal), um tema claro ou escuro, etc. Além disso, através deste wrapper, você pode acessar o banco de dados ao usar o CoreData:

 @Environment(\.managedObjectContext) var moc: NSManagedObjectContext 

A propósito, muitas coisas interessantes foram feitas para trabalhar com o CoreData no SwiftUI. Mas sobre isso, talvez, da próxima vez. Portanto, o artigo cresceu além de todas as expectativas.

Custom @PropertyWrapper


Em geral, PropertyWrapperesse é um atalho para setter e getter, o mesmo para todos os parâmetros agrupados no mesmo property wrapper. Você pode restaurar completamente essa funcionalidade removendo a declaração do wrapper e gravando os parâmetros getter {} setter {}, mas será necessário fazer isso sempre que Viewduplicar o código. Por exemplo, é PropertyWrappermuito conveniente ocultar o trabalho UserDefaults.

 @propertyWrapper struct UserDefault<T> { var key: String var initialValue: T var wrappedValue: T { set { UserDefaults.standard.set(newValue, forKey: key) } get { UserDefaults.standard.object(forKey: key) as? T ?? initialValue } } } 

Assim, podemos armazenar tipos de dados primitivos no armazenamento UserDefaults. A Apple alega uma velocidade de acesso muito boa a esse armazenamento, portanto, provavelmente não é necessário armazenar em cache esses dados na memória na forma de parâmetros ou variáveis ​​de estrutura, a menos que sejam usados ​​em loops maciços e tarefas que exigem velocidade.

Dado isso, você pode criar um tipo de stub (neste caso, uma enumeração) no qual declarar variáveis ​​estáticas para acessar valores específicos armazenados UserDefaults, usando o wrapper recém-criado:

 enum UserPreferences { @UserDefault(key: "isCheatModeEnabled", initialValue: false) static var isCheatModeEnabled: Bool @UserDefault(key: "highestScore", initialValue: 10000) static var highestScore: Int @UserDefault(key: "nickname", initialValue: "cloudstrife97") static var nickname: String } 

O resultado pode ser usado de forma muito sucinta, com foco na exibição lógica e visual, e todo o trabalho é realizado sob o capô.

 UserPreferences.isCheatModeEnabled = true UserPreferences.highestScore = 25000 UserPreferences.nickname = "squallleonhart” 

Um exemplo foi originalmente descrito aqui .

Contentores


Bem, a última coisa a discutir da minha lista são os contêineres. Já tocamos parcialmente nisso quando falamos sobre body. De fato, os contêineres são comuns View. A única diferença é que, como um dos parâmetros dessa estrutura, transmitimos o conteúdo. Lembre-se de que o conteúdo é um fechamento que contém uma ou mais expressões declarativas. Esse fechamento, se processado com a ajuda de @ViewBuilder, retornará para nós uma nova Visualização, combinando de uma certa maneira toda a Visualização listada no fechamento (blocos de conteúdo). Ao mesmo tempo, para contêineres diferentes, os mecanismos de processamento dos próprios blocos são diferentes. VStackorganiza elementos de conteúdo verticalmente, HStackhorizontalmente e assim por diante. É como um modificador, mas desta vez não é apenas uma Visualização específica que está sendo modificada, mas o todoContenttransferido para o contêiner e um novo é gerado View. Além disso, este novo Viewtem um novo tipo. Por exemplo, para HStack{Text(…)}este tipo será TupleView<Text, Image>.

No entanto, não esqueça que qualquer um View, incluindo contêineres, é uma estrutura que pode ter outros parâmetros além do corpo. Por exemplo, durante muito tempo não consegui descobrir como remover um pequeno espaço entre o Text(«a») Text(«b»)interior HStack. Passei muito tempo com offset()e position(), calculando as coordenadas do deslocamento com base no comprimento das linhas, até encontrar acidentalmente a sintaxe completa da declaração HStack:
HStack (espaçamento:, alinhamento:, contexto :).
Simplesmente, os dois primeiros parâmetros são opcionais e são ignorados na maioria dos exemplos. Erro do novato - para não ver a sintaxe completa.

Foreach


Separadamente, vale a pena falar ForEach. Este é um contêiner que serve para refletir na tela todos os elementos da coleção transferida. Primeiro de tudo, você precisa entender que isso não é o mesmo que pedir alguma coleta forEach(…). Como dissemos acima, ele ForEachretorna um único View, criado com base nos elementos da coleção transferida. I.e.é apenas outro contêiner para o qual a coleção é transferida e instruções sobre como refletir os elementos da coleção na tela.
Além disso, ele ForEachdeve ser colocado dentro de algum outro contêiner que já determine como agrupar essa entidade múltipla - colocando-o na vertical, na horizontal ou, por exemplo, em uma lista ( List).

ForEachaceita três parâmetros: coleção ( data: RandomAccesCollection), endereço do identificador do elemento de coleção ( id: Hashable) e conteúdo ( content: ()->Content). O terceiro que já discutimos: como qualquer outro contêiner, ele ForEachaceita Content- ou seja, curto-circuito. Porém, diferentemente dos contêineres comuns, onde contentnão contém parâmetros, ele ForEachpassa para o fechamento um elemento de coleção que pode ser usado para descrever o conteúdo.

A coleção ForEachnão é adequada para nenhum, mas apenas RandomAccesCollection. Para várias coleções desordenadas, basta chamar o método sorted(by:)com o qual você pode obter RandomAccesCollection.

ForEach- Este é um conjunto subViewgerado para cada elemento da coleção com base no conteúdo transmitido. É importante observar que o SwiftUI precisa saber qual deles subViewestá associado a qual elemento da coleção. Para isso, cada um Viewdeve ter um identificador. O segundo parâmetro é necessário precisamente para isso. Se os elementos da coleção forem Hashabletipos, como seqüências de caracteres, você poderá escrever simplesmente id: \.self. Isso significa que a própria string será o identificador. Se os elementos da coleção são classes e satisfazem o protocoloIdentifiable- então o segundo argumento pode ser perdido. Nesse caso, o ID de cada item da coleção se tornará um identificador subView. Se o seu objeto tiver algum tipo de acessório que forneça exclusividade e que atenda ao protocolo Hashable, você poderá especificá-lo assim:

 ForEach(values, id: \.value){item in …} 

No meu exemplo, valuesé uma matriz de objetos de classe SomeObjectpara os quais props são declarados value: Int. De qualquer forma, você deve garantir que cada identificador Viewassociado a um elemento da sua coleção seja exclusivo . Por exemplo, no seu contexto, alguns parâmetros do seu objeto podem mudar. Viewele deve corresponder 1 a 1 ao objeto de dados (elemento de coleção), caso contrário, não ficará claro para onde retornar a alteração do @Bindingparâmetro View.

A propósito, a organização de um rastreamento de elementos da coleção que não atendem ao Identificável também pode ser feita usando índices. Por exemplo, assim:
 ForEach(keys.indices){ind in SomeView(key: self.keys[ind]) } 

Nesse caso, a travessia será construída não pelos próprios elementos, mas por seus índices. Para coleções pequenas, isso é perfeitamente aceitável. Em coleções com um grande número de elementos, isso provavelmente pode afetar o desempenho, especialmente quando os elementos da coleção não são tipos de referência, mas, por exemplo, cadeias volumosas ou dados JSON. Em geral, use com cuidado.

Um ponto importante sobre o Contentque é referido ForEach. Ele é muito mal-humorado e se recusa a trabalhar normalmente com fechamentos, com mais de um bloco (ou seja, ele percebe o conteúdo de uma linha normalmente, mas ele já não tem 2 ou mais). Isso é resolvido de maneira bem simples, é bastante simples Groupe{}incluir todo o conteúdo - esse hack deixa de ser um problema.

Basta declarar que as variáveis ​​internas no escopo deste fechamento não funcionarão. Quaisquer fechamentos passados ​​para o ViewBuilder não podem conter declarações de variáveis. Lembre-se, no começo do artigo, dei um exemplo de criação de um modificador .frame(size:)? Eu criei por esse motivo. Calculei os tamanhos dos botões com base no número desses botões em uma linha e no número de linhas (não fiquei satisfeito com o alongamento automático, botões diferentes devem ter tamanhos diferentes). A função retornou CGSize e vários níveis de estruturas aninhadas foram rastreados para dentro. Se fosse possível executar a função uma vez, escreva o resultado como um tamanho variável e chame.ftame(width: size.width, height: size.height)"Eu faria isso." Mas não existe essa possibilidade, e eu não queria executar a função duas vezes - porque contornei essa limitação e coloquei parte do código no modificador.

Visualização de contêiner personalizado


Bem, como aconteceu, darei um exemplo de criação de um contêiner personalizado. Muitas vezes, o relacionamento de vários objetos do tipo "1: N" pode ser convenientemente representado na forma de um dicionário. Não é dict: [KeyObject: [SomeObject]]difícil executar a consulta e converter seu resultado em um dicionário de tipos .

Nesse caso, os objetos da classe atuam como a chave do dicionário KeyObject(para isso, ele deve suportar o protocolo Hashable), e os valores são matrizes de objetos de outra classe - SomeObject.

 class SomeObject: Identifiable{ let value: Int public let id: UUID = UUID() init(value: Int){ self.value = value } } class KeyObject: Hashable, Comparable{ var name: String init(name: String){ self.name = name } static func < (lhs: KeyObject, rhs: KeyObject) -> Bool { lhs.name < rhs.name } static func == (lhs: KeyObject, rhs: KeyObject) -> Bool { return lhs.name == rhs.name } func hash(into hasher: inout Hasher) { hasher.combine(name) } } 

Se seu aplicativo planeja algum tipo de análise com agrupamento, faz sentido criar um contêiner separado para exibir esses dicionários, para não duplicar todo o código em cada exibição. E, considerando que os agrupamentos podem ser alterados pelo usuário, teremos que usar genéricos. Não compliquei adicionando design visual, deixando apenas a estrutura do nosso contêiner:

 struct TreeView<K: Hashable, V: Identifiable, KeyContent, ValueContent>: View where K: Comparable, KeyContent: View, ValueContent: View{ let data: [K: [V]] let keyContent: (K)->KeyContent let valueContent: (V)->ValueContent var body: some View{ VStack(alignment: .leading, spacing: 0){ ForEach(data.keys.sorted(), id: \.self){(key: K) in VStack(alignment: .trailing, spacing: 0){ self.keyContent(key) ForEach(self.data[key]!){(value: V) in self.valueContent(value) } } } } } } 

Como você pode ver, o contêiner aceita um dicionário do tipo [K: [V]](onde Ké o tipo de objetos-chave do dicionário, Vé o tipo de matriz que os valores do dicionário consistem) e dois contextos: um para exibir as chaves do dicionário e o outro para exibir os valores. Infelizmente, não encontrei exemplos de criação ViewBuilder-de contêineres personalizados (provavelmente essa opção simplesmente não existe), portanto, teremos que usar o padrão ForEach. Como ele aceita apenas na entrada RandomAccessCollection, e dict.keysnão é, teremos que usar a classificação. Daí o requisito de suportar o protocolo Comparablek KeyObject.

Eu usei dois ForEachcontêineres aninhados . No primeiro caso, usei o hash do item de coleção (\.self) como o identificador de cada aninhado View. Eu poderia fazer isso porque as chaves de dicionário devem suportar o protocolo de qualquer maneira Hashable. No segundo caso, adicionei SomeObjectsuporte de protocolo à classeIdentifiable. Isso me permitiu não especificar uma chave de comunicação - o id é usado automaticamente. No meu caso, o id não é armazenado em nenhum lugar. Cada vez que um objeto é criado - seja criação no código ou recuperação usando uma consulta ao banco de dados - um novo ID é gerado. Para uma interface, isso não é essencial. Não mudará ao longo da vida útil do objeto, ou seja, sessão, e isso é suficiente para exibi-lo sob esse ID. E se da próxima vez que você abrir o aplicativo, ele terá um ID diferente - nada de ruim acontecerá. Se o seu objeto já possui campos-chave, você pode simplesmente tornar a id um parâmetro calculado e usar o suporte a este protocolo e uma sintaxe abreviada de qualquer maneira ForEach.

Um exemplo de como usar nosso contêiner:

 struct ContentView: View { let dict: [KeyObject: [SomeObject]] = [ KeyObject(name: "1st group") : [SomeObject(value: 1), SomeObject(value: 2), SomeObject(value: 3)], KeyObject(name: "2nd group") : [SomeObject(value: 4), SomeObject(value: 5), SomeObject(value: 6)], KeyObject(name: "3rd group") : [SomeObject(value: 7), SomeObject(value: 8), SomeObject(value: 9)] ] var body: some View { TreeView(data: dict, keyContent: {keyObject in Text("the key is: \(keyObject.name)") } ){valueObject in Text("value: \(valueObject.value)") } } } 

e o resultado na tela no Canvas:



para ser continuado


Por enquanto é tudo. Eu também queria cobrir todos os ancinhos em que pisei, tentando usá-lo CoreDataem conjunto com o SwiftUI, mas, francamente, não esperava que apenas o básico do SwiftUI levasse tanto tempo e que o artigo fosse tão volumoso. Então, como eles dizem, para continuar.

Se você tem algo a acrescentar ou corrigir - seja bem-vindo aos comentários. Vou tentar refletir comentários significativos no artigo.

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


All Articles