SwiftUI para a última tarefa competitiva do Telegram Charts (março de 2019): tudo é simples



Começarei com a observação de que o aplicativo discutido neste artigo requer o Xcode 11 e o MacOS Catalina, se você quiser usar as Live Previews , e o Mojave se você usar o simulador. O código do aplicativo está no Github .

Este ano, na WWDC 2019 , a Apple anunciou o SwiftUI , uma nova maneira declarativa de criar uma interface de usuário (UI) em todos os dispositivos Apple . É quase uma partida completa do UIKit usual, e eu - como muitos outros desenvolvedores - realmente queria ver essa nova ferramenta em ação.

Este artigo apresenta a experiência de resolver com o SwiftUI um problema cujo código no UIKit incomparavelmente mais complexo e não pode ser UIKit na minha opinião de maneira legível.

A tarefa está relacionada ao último concurso Telegram para desenvolvedores de Android , iOS e JS , realizado de 10 a 24 de março de 2019. Nesta competição, uma tarefa simples foi proposta para exibir graficamente a intensidade de uso de um determinado recurso na Internet, dependendo do tempo com base nos dados JSON . Como desenvolvedor do iOS , você deve usar o Swift para enviar o código escrito do zero para a competição sem usar bibliotecas especializadas externas para plotagem.

Essa tarefa exigia habilidades para trabalhar com os recursos gráficos e de animação do iOS: Core Graphics , Core Animation , Metal , OpenGL ES . Algumas dessas ferramentas são ferramentas de programação de baixo nível e não orientadas a objetos. Basicamente, no iOS não havia modelos aceitáveis ​​para resolver tarefas gráficas aparentemente leves à primeira vista. Portanto, cada competidor inventou seu próprio animador ( Render ) com base em Metal , CALayers , OpenGL , CADisplayLink . Isso gerou toneladas de código dos quais não foi possível emprestar e desenvolver nada, uma vez que esses são trabalhos puramente "protegidos por direitos autorais" que somente autores podem realmente desenvolver. No entanto, isso não deve ser assim.

E no início de junho na WWDC 2019 , o SwifUI aparece - uma nova framework desenvolvida pela Apple , escrita em Swift e projetada para descrever declarativamente a interface do usuário ( UI ) no código. Você determina quais subviews exibidas em sua View , quais dados fazem com que essas subviews sejam alteradas, quais modificadores você precisa aplicar a eles, para posicioná-los no lugar certo, para ter o tamanho e o estilo certos. Um elemento igualmente importante do SwiftUI é o controle do fluxo de dados modificáveis ​​pelo usuário, que por sua vez atualiza a UI do UI .

Neste artigo, quero mostrar como a própria tarefa do concurso Telegram no SwiftUI é resolvida de maneira rápida e fácil. Além disso, este é um processo muito emocionante.

Tarefa


O aplicativo competitivo deve exibir simultaneamente 5 "conjuntos de gráficos" na tela usando os dados fornecidos pelo Telegram . Para um "conjunto de gráficos", a UI do UI seguinte:



Na parte superior, há uma "zona do gráfico" com uma escala comum ao longo do eixo Y normal, com marcas e linhas de grade horizontais. Abaixo disso, há uma linha rasteira com carimbos de data / hora ao longo do eixo X como datas.

Ainda mais baixo é o chamado “mini mapa” (como no Xcode 11 ), ou seja, uma “janela” transparente que define a parte do período de nossos “Gráficos”, que é apresentada com mais detalhes na “zona Gráficos” superior. Este “mini mapa” não só pode ser movido ao longo do eixo X , mas também sua largura pode ser alterada, o que afeta a escala de tempo na “área Gráficos”.

Com a ajuda de checkboxs de checkboxs pintadas nas cores de "Gráficos" e fornecidas com seus nomes, você pode recusar a exibição dos "Gráficos" correspondentes a essa cor na "zona Gráficos".

Existem muitos desses "conjuntos de gráficos"; no nosso exemplo de teste, existem 5 deles, por exemplo, e todos devem estar localizados em uma tela.

Na UI do UI projetada usando o SwiftUI não há necessidade de um botão para alternar entre os modos Dark e Light , isso já está incorporado no SwiftUI . Além disso, o SwiftUI muito mais opções para combinar "conjuntos de gráficos" (ou seja, os conjuntos de telas apresentados acima) do que simplesmente rolar uma tabela para baixo, e examinaremos algumas dessas opções muito interessantes.

Mas primeiro, vamos nos concentrar em exibir um "conjunto de SwiftUI " para o qual criaremos um ChartView :



SwiftUI permite criar e testar uma UI do UI complexa em pedaços pequenos e, em seguida, é muito fácil montar essas peças em um quebra-cabeça. Nós faremos isso. Nosso ChartView muito bem em pequenos pedaços:

  • GraphsForChart - estes são os próprios gráficos, criados para um "conjunto de gráficos" específico. "Gráficos" são mostrados para o intervalo de tempo controlado pelo usuário usando o RangeView "mini mapa", que será apresentado abaixo.
  • YTickerView é o eixo Y com elevações e a grade horizontal correspondente.
  • IndicatorView é um IndicatorView horizontal orientado ao usuário que permite visualizar os valores de "Gráficos" e o tempo da posição correspondente do indicador no eixo do tempo no eixo X
  • TickerView - "linha rasteira" mostrando carimbos de data e hora no eixo X como datas,
  • RangeView - uma "janela" temporária, personalizável pelo usuário usando gestos, para definir o intervalo de tempo para "Gráficos",
  • CheckMarksView - contém "botões" coloridos nas cores de "Gráficos" e permite controlar a presença de " ChartView " no ChartView .

ChartView usuário pode interagir com o ChartView de três maneiras:

1. controle o “mini mapa” usando o gesto DragGesture - ele pode mudar a “janela” temporária para a direita e esquerda e diminuir / aumentar seu tamanho:



2. mova o indicador na direção horizontal, mostrando os valores dos "Gráficos" em um ponto fixo no tempo:



3. oculte / mostre certos "Gráficos" usando botões coloridos nas cores "Gráficos" e localizados na parte inferior do ChartView :



Podemos combinar vários "Conjuntos de gráficos" (temos cinco deles em dados de teste) de diferentes maneiras, por exemplo, colocando-os todos simultaneamente em uma tela usando a List Lista (como uma tabela rolável para cima e para baixo):



ou usando o ScrollView e uma pilha horizontal do HStack com um efeito 3D:



... ou na forma de um ZStack "cartões" sobrepostos, cuja ordem pode ser alterada: o "cartão" superior com "" um conjunto de gráficos "pode ​​ser puxado para baixo o suficiente para olhar para o próximo cartão e, se você continuar a arrastá-lo para baixo, então" vai "para o último lugar no ZStack , e este próximo" cartão "" segue em frente ":



Nessas UI complexas - uma “mesa rolável”, uma pilha horizontal com efeito 3D , um ZStack “cartões” sobrepostos uns aos outros - todos os meios de interação do usuário funcionam completamente: movendo-se na linha do tempo e alterando a “escala” dos ZStack , indicador e ocultar "Gráficos".

Além disso, consideraremos em detalhes o design dessa UI do UI usando o SwiftUI - de elementos simples a composições mais complexas. Mas primeiro, vamos entender a estrutura de dados que temos.

Portanto, a solução para o nosso problema foi dividida em várias etapas:

  • Faça o download dos dados de um arquivo JSON e apresente-os em um formato "interno" conveniente
  • Criar UI do UI para um "conjunto de gráficos"
  • Combine vários "conjuntos de gráficos"

Baixar dados


À nossa disposição, o Telegram forneceu dados JSON contendo vários "conjuntos de gráficos". Cada "conjunto de chart " individual de um chart contém vários "Gráficos" (ou "Linhas") de chart.columns . Cada "Gráfico" ("Linhas") tem uma marca na posição 0 - "x" , "y0" , "y1" , "y2" , "y3" , seguida pelos valores de tempo no eixo X ("x") ou os valores de "Gráficos" ("Linhas") ( "y0" , "y1" , "y2" , "y3" ) no eixo Y :



A presença de todas as "Linhas" no "conjunto de gráficos" é opcional. Os valores para a "coluna" x são registros de data e hora do UNIX em milissegundos.

Além disso, cada "conjunto de chart " individual do chart é fornecido com cores chart.colors no formato de 6 dígitos hexadecimais (por exemplo, "#AAAAAA") e chart.names .

Para criar o Modelo de Dados localizado no arquivo JSON , usei o excelente serviço quicktype . Neste site, você insere um pedaço de texto de um arquivo JSON e especifica a linguagem de programação ( Swift ), o nome da estrutura ( Chart ), que será formada após a "análise" desses dados JSON e é isso.

Um código é gerado na parte central da tela, que Chart.swift em nosso aplicativo em um arquivo separado chamado Chart.swift . É aqui que colocaremos o Modelo de Dados no formato JSON. Usando o carregador de dados do arquivo JSON para o modelo emprestado das demos SwiftUI Generic , obtive uma matriz de columns: [ChartElement] , que é uma coleção de "conjuntos de columns: [ChartElement] " no formato de Telegram .

A ChartElement dados ChartElement , contendo matrizes de elementos heterogêneos, não é muito adequada para trabalhos interativos intensivos com gráficos; além disso, os carimbos de data e hora são apresentados no formato UNIX em milissegundos (por exemplo, 1542412800000, 1542499200000, 1542585600000, 1542672000000 ) e cores no formato hexadecimal 6 dígitos (por exemplo, "#AAAAAA" ).

Portanto, dentro de nossa aplicação, usaremos os mesmos dados, mas em um formato "interno" e bastante simples diferente [LinesSet] . A matriz [LinesSet] é uma coleção de "Conjuntos de LinesSet " de LinesSet , cada um dos quais contém xTime data xTime hora xTime no formato "Feb 12, 2019" (eixo X ) e várias lines "Gráficos" (eixo Y ):



Os dados para cada gráfico de linhas (linha) são apresentados

  • uma matriz de points: [Int] inteiros points: [Int] ,
  • chamado title: String "Gráficos" title: String ,
  • tipo "Graphics" type: String? ,
  • color : UIColor no formato UIColor do Swift ,
  • número de pontos countY: Int .

Além disso, qualquer "Gráfico" pode ser oculto ou exibido, dependendo do valor de isHidden: Bool . Os upperBound e upperBound ajustar o intervalo de tempo levam valores de 0 a 1 e mostram não apenas o tamanho da janela de tempo do “mini mapa” ( upperBound - lowerBound ), mas também sua localização no eixo temporal X :



As estruturas de dados JSON [ChartElement] e as estruturas de dados das LinesSet e Line "internas" estão no arquivo Chart.swift . O código para carregar dados JSON e convertê-los em uma estrutura interna está localizado no arquivo Data.swift . Detalhes sobre essas transformações podem ser encontrados aqui .

Como resultado, recebemos dados sobre os "conjuntos de gráficos" no formato interno como uma matriz de chartsData .



Este é o nosso Dados, mas, para trabalhar no SwiftUI , é necessário garantir que quaisquer alterações feitas pelo usuário na matriz chartsData (alterando a "janela" temporária, ocultando / mostrando "Gráficos") levem a atualizações automáticas de nossos chartsData exibição.

@EnvironmentObject . Isso nos permitirá usar o Dados sempre que necessário e, além disso, atualizaremos automaticamente nossas Views se os dados forem alterados. Isso é algo como Singleton ou dados globais.

@EnvironmentObject exige que @EnvironmentObject uma final class UserData , localizada no arquivo UserData.swift , armazene os dados chartsData e implemente o protocolo ObservableObject :



A presença de "wrappers" do @Published permitirá que você publique "notícias" que essas propriedades dos charts da classe UserData foram alteradas, para que quaisquer Views "subscritas a esta notícia" no SwiftUI possam selecionar automaticamente novos dados e atualizar.

Lembre-se de que na propriedade de charts os valores isHidden de qualquer " isHidden " podem mudar (eles permitem ocultar ou mostrar esses "Gráficos"), bem como os limites de tempo inferior inferior e superior superior para cada "conjunto de gráficos" individual.

Queremos usar a propriedade de charts da classe UserData todo o aplicativo e não precisamos sincronizá-los com a UI do UI manualmente, graças a @EnvironmentObject .

Para fazer isso, ao iniciar o aplicativo, precisamos criar uma instância da classe UserData () para que posteriormente possamos acessá-lo em qualquer lugar do aplicativo. Faremos isso no arquivo SceneDelegate.swift dentro do método scene (_ : , willConnectTo: , options: ) . É aqui que nosso ContentView é criado e lançado, e é aqui que devemos passar o @EnvironmentObject ContentView qualquer @EnvironmentObject que @EnvironmentObject para que o SwiftUI possa disponibilizá-los para qualquer outra View :



Agora, em qualquer View para acessar os dados @Published da classe UserData , precisamos criar a variável var usando o wrapper @EnvironmentObject . Por exemplo, ao definir o intervalo de tempo no RangeView criamos a variável var userData com o UserData TYPE:



Portanto, assim que implementamos algum @EnvironmentObject no "ambiente" do aplicativo, podemos começar a usá-lo imediatamente no nível mais alto ou no 10º nível abaixo - isso não importa. Mais importante, porém, sempre que uma View muda o "ambiente", todas as Views que possuem esse @EnvironmentObject serão @EnvironmentObject automaticamente, garantindo a sincronização com os dados.

Vamos seguir para o design da interface do usuário ( UI ).

Interface do usuário (UI) para um "conjunto de gráficos"


SwiftUI oferece uma tecnologia composta para criar SwiftUI de SwiftUI partir de muitas pequenas Views , e já vimos que nosso aplicativo se encaixa muito bem com essa tecnologia, pois se divide em pequenas partes: o conjunto " ChartView Charts", " GraphsForChart Charts", Y YTickerView - YTickerView , indicador acionado pelo usuário dos valores "Graphs" IndicatorView , linha " TickerView " em execução com TickerView data e hora no eixo X , RangeView "janela de tempo" acionado pelo usuário, marcas para ocultar / mostrar o CheckMarksView "Graphs". Não podemos apenas criar todas essas Views independentemente uma da outra, mas também testar imediatamente no Xcode 11 usando Previews ( Previews preliminares "ao vivo") nos dados de teste. Você ficará surpreso com a simplicidade do código para criá-los a partir de outras Views mais básicas.

GraphView - "Gráfico" ("Linha")


A primeira View , com a qual começaremos, é na verdade o próprio “Gráfico” (ou “Linha”). Vamos chamá-lo de GraphView :



A criação de um GraphView , como sempre, começa com a criação de um novo arquivo no Xcode 11 usando o menu FileNewFile :



Em seguida, selecionamos o TIPO desejado do arquivo - este é o arquivo SwiftUI :



... atribua o nome "GraphView" à nossa View e indique sua localização:



Clique no botão "Create" e obtenha uma View padrão com Text ( "Hello World!") No meio da tela:



Nossa tarefa é substituir o Text ("Hello World!") do Text ("Hello World!") "Graph", mas primeiro vamos ver quais dados iniciais temos para criar o "Graph":

  • nós temos os valores de line.points "Graphics" line: Line ,
  • intervalo de tempo rangeTime , que é um intervalo de índices Range carimbos Range hora xTime no eixo X,
  • faixa de valores rangeY: Range “Graphics” para o rangeY: Range Y,
  • espessura da linha da linha de traçado "Gráficos"

Adicione essas propriedades à estrutura GraphView :



Se quisermos usar para nossas Previews "Gráficos" (visualizações), que são possíveis apenas para o MacOS Catalyna , devemos iniciar um GraphView com o intervalo de índices rangeTime e os dados de line dos próprios "Gráficos":



Já temos os dados de teste chartsData que obtivemos do arquivo JSON chart.json e usamos para Previews - Previews .

No nosso caso, este será o primeiro "conjunto de chartsData[0] " chartsData[0] e o primeiro "Gráfico" neste conjunto de chartsData[0].lines[0] , que forneceremos o GraphView como o parâmetro de line .

Usaremos todo o intervalo de índices 0..<(chartsData[0].xTime.count - 1) como intervalo de tempo rangeTime .
Os lineWidth e lineWidth podem ser definidos externamente ou não, pois já possuem valores iniciais: rangeY é nil e lineWidth é 1 .

Intencionalmente, rangeY um TYPE da propriedade rangeY Propriedade Optional TYPE, porque se rangeY não rangeY definido externamente e rangeY = nil , calcularemos os valores mínimo minY e máximo maxY dos "Gráficos" diretamente a partir da line.points .



Esse código é compilado, mas ainda temos uma View padrão na tela com o texto Text ("Hello World!") No meio da tela:



Porque no body temos que substituir o texto Text ("Hello World!") Path , que criará nosso "Graph: points: line.points usando o addLines(_:) (quase como no Core Graphics )




lineWidth stroke (...) nosso Path linha cuja espessura é lineWidth , e a cor da linha do traçado corresponderá à cor padrão (ou seja, preto):



Podemos substituir a cor preta da linha de traçado pela cor especificada em nosso line.color "Linha" específico "Cor":



Para que nosso "Gráfico" seja colocado em retângulos de qualquer tamanho, usamos o contêiner GeometryReader . Na documentação da Apple GeometryReader é uma View "contêiner" que define seu conteúdo como uma função de seu próprio tamanho de size e espaço de coordenadas. Essencialmente, GeometryReader é outra View ! Porque quase tudo no SwiftUI é View ! GeometryReader permitirá que você, diferentemente de outras Views acesse algumas informações adicionais úteis que você pode usar ao criar sua View personalizada.

Usamos os contêineres GeometryReader e Path para criar o GraphView adaptável a qualquer tamanho. E se olharmos atentamente para o nosso código, veremos no fechamento do GeometryReader variável chamada geometry :



Essa variável possui o GeometryProxy TYPE, que por sua vez é uma estrutura struct com muitas "surpresas":

 public var size: CGSize { get } public var safeAreaInsets: EdgeInsets { get } public func frame(in coordinateSpace: CoordinateSpace) -> CGRect public subscript<T>(anchor: Anchor<T>) -> T where T : Equatable { get } 

A partir da definição de GeometryProxy , vemos que existem duas variáveis ​​computadas var size e var safeAreaInsets , um frame( in:) função frame( in:) e um subscript getter . Precisamos apenas da variável de size para determinar a largura da largura geometry.size.width e altura da geometry.size.height área de desenho “Graphics”.

Além disso, permitimos que nosso “Gráfico” seja animado usando o modificador de animation (.linear(duration: 0.6)) .



GraphView_Previews nos permite testar quaisquer "Gráficos" de qualquer "conjunto" de maneira muito simples. Abaixo está o "Gráfico" do "conjunto de gráficos" com o índice 4: chartsData[4] e o índice 0 "Gráficos" neste conjunto: chartsData[4].lines[0] :



Definimos a height “Gráficos” para 400 usando o frame (height: 400) , a largura permanece a mesma que a largura da tela. Se não usamos o frame (height: 400) , o "Gráfico" ocuparia a tela inteira.Não especificamos um intervalo de valores rangeYe GraphViewusamos o valor nilpadrão. Nesse caso, o "Gráfico" leva seus valores mínimo e máximo no intervalo de tempo rangeTime:



embora tenhamos usado um Pathmodificador para o nosso modelo animation (.linear(duration: 0.6)), nenhuma animação ocorrerá, por exemplo, ao alterar o intervalo de rangeYvalores " Gráficos ". Um "gráfico" simplesmente "salta" de um valor de um intervalo rangeYpara outro sem nenhuma animação.

O motivo é simples: ensinamos SwiftUIcomo desenhar um “Gráfico” para um intervalo específico rangeY, mas não ensinamos SwiftUIcomo reproduzir um “Gráfico” várias vezes com valores intermediários do intervalo rangeYentre o início e o fim, e para isso emSwiftUIatende ao protocolo Animatable.

Felizmente, se a sua Viewé uma "figura", isto é View, que implementa um protocolo Shape, então um protocolo já foi implementado para ele Animatable. Isso significa que existe uma propriedade computada animatableDatacom a qual podemos controlar o processo de animação, mas, por padrão, é definida como EmptyAnimatableData, ou seja, nenhuma animação ocorre.

Para resolver o problema da animação, primeiro precisamos transformar nosso "Gráfico" GraphViewem Shape. É muito simples, precisamos apenas implementar a função func path (in rect:CGRect) -> Pathque já possuímos e indicar com a ajuda da propriedade computada animatableDataquais dados queremos animar:



Observe que o tema do controle de animação é um tópico avançado emSwiftUIe você pode aprender mais sobre isso no artigo “Animações avançadas do SwiftUI - Parte 1: Caminhos” . Podemos usar a

"figura" resultante Graphem um GraphViewNew"Gráficos" muito mais simples com animação:



você vê que não precisamos GeometryReaderdos nossos novos "Gráficos" GraphViewNew, porque, graças ao protocolo, Shapenossa "figura" Graphpoderá se adaptar a qualquer tamanho do pai View.

Naturalmente, Previewsobtivemos o mesmo resultado que no caso de GraphView:



Nas seguintes combinações, usaremos os GraphViewNewmesmos "Gráficos" para exibir os valores.

GraphsForChart - conjunto de "gráficos" ("linhas")


O objetivo deste View- Exibe todos "Gráficos" ( "linhas") de "ajustou gráficos" chartem um intervalo de tempo pré-determinado rangeTimecom um eixo comum Y, a largura das "linhas" é igual a lineWidth:



Tal como acontece com GraphViewe GraphViewNewvamos criar GraphsForChartum novo arquivo GraphsForChart.swift, e definir os dados de entrada para "Conjunto de gráficos":

  • o próprio "conjunto de gráficos" chart: LineSet(valores ativados Y),
  • intervalo rangeTime: Range( X) de índices dos carimbos de hora de "Gráficos",
  • espessura do traçado da linha do gráfico lineWidth

O intervalo de valores rangeY: Rangepara o "conjunto de gráficos" ( Y) é calculado como a união dos intervalos do indivíduo não oculto ( isHidden = false) "Gráficos" incluídos neste "conjunto":



Para isso, usamos a função rangeOfRanges: Mostramos



todos os gráficos NÃO ocultos ( isHidden = false) na ZStackconstrução ForEach, dando a cada “gráfico” a possibilidade de aparecer na tela e sair da tela “usando o modificador“ mover ” transition(.move(edge: .top)):



graças a esse modificador, o processo de ocultar e retornar o“ gráfico ” ChartViewocorrerá na tela com animação e deixará claro para o usuário por que a escala mudou Y.

Use drawingGroup()significa useMetalpara desenhar formas gráficas. Em nossos dados de teste e no simulador, você não sentirá a diferença na velocidade do desenho com Metale Metal, mas se você reproduzir muitos gráficos bastante volumosos em qualquer um iPhonedeles, notará essa diferença. Para uma introdução mais detalhada, quando usá-lo drawingGroup(), você pode ver o artigo "Animações avançadas do SwiftUI - Parte 1: Caminhos" ou assistir à sessão de vídeo 237 WWDC 2019 ( Criando visualizações personalizadas com o SwiftUI ).

Como no caso de GraphViewNewtestes GraphsForChartusando visualizações, Previewspodemos defina qualquer "conjunto de gráficos", por exemplo, com um índice 0:



IndicatorView - indicador "Gráficos" movido horizontalmente.


Este indicador permite obter os valores exatos de "Gráficos" e o tempo para o ponto correspondente no momento X:



O indicador é criado para um "conjunto de gráficos" específico charte consiste em mover-se ao longo da XLINHA vertical com MARCAS na forma de "círculos" no lugar dos valores de "Gráficos". Um pequeno "POSTER" é anexado ao topo desta linha vertical, contendo os valores numéricos dos "Gráficos" e da hora.



O indicador desliza pelo usuário usando um gesto DragGesture:



Usamos a chamada execução de gesto “incremental”. Em vez de uma distância contínua do ponto de partida value.translation.width, onChangedreceberemos constantemente a distância do local em que estávamos na última vez em que executamos o gesto no manipulador :value.translation.width - self.prevTranslation. Isso nos fornecerá um movimento suave do indicador.

Para testar o indicador IndicatorViewcom a ajuda de um Previewsdeterminado "conjunto de gráficos", chartpodemos atrair a Viewconstrução pronta de "gráficos" GraphsForChart:



podemos definir qualquer intervalo de tempo, mas coordenado entre si, para rangeTimeo indicador IndicatorViewe "gráficos" GraphsForChart. Isso nos permitirá garantir que os "círculos" que indicam os valores dos "Gráficos" estejam nos lugares certos.

TickerView- Xcom marcas.


Até o momento, nossos “Gráficos” são despersonalizados no sentido de que NÃO X Ypossuem as escalas e marcas apropriadas. Vamos desenhar Xcom timestamps TickerMarkViewnele. marca Sami TickerMarkViewsão muito simples Viewpilha vertical VStackem que estão dispostas Pathe Text:



O conjunto de marcas sobre o eixo de tempo para um certo "conjunto de gráficos" chart : LineSetformados TickerViewde acordo com o intervalo de tempo seleccionado pelo utilizador rangeTimee a quantidade aproximada de marcas estimatedMarksNumber, os quais devem estar no campo de visão do utilizador:



Para arranjo Carimbos de data e hora em execução, usamos uma ScrollViewpilha horizontalHStack, que mudará conforme o intervalo de tempo for alterado rangeTime.

Como TickerViewse formar um degrau step, que aparecem selo de tempo TimeMarkViewcom base num intervalo de tempo predeterminado rangeTimee a largura da tela widthRange...



... e, em seguida, seleccionar o tempo do passo c selo stepda matriz chart.xTimepor meio do índice indexes.

Na verdade X- uma linha horizontal - vamos colocar overlay...



... em uma pilha horizontal HStack, com registros de data TimeMarkViewe hora , com os quais avançamos offset:



Além disso, podemos definir as cores do X- em si colorXAxise as marcas - colorXMark:



YTickerView- Ycom marcas e uma grade.


Este Viewdesenha Ycom marcas digitais YMarkView. As próprias marcas YMarkViewsão muito simples, Viewcom uma pilha vertical VStackna qual são colocadas Path(linha horizontal) e Textcom um número:



Um conjunto de marcas ativadas Ypara um determinado "conjunto de tabelas" charté formado YTickerView. O intervalo de valores rangeYé calculada como a união de todas as gamas de valores "Graphics", incluído no "conjunto de gráficos" com a função rangeOfRanges. O número aproximado de marcas no eixo Y é definido pelo parâmetro estimatedMarksNumber:



 YTickerViewmonitoramos a alteração no intervalo dos valores de "Gráficos" rangeY. Na verdade, o eixo Y - a linha vertical - impomos overlayàs nossas marcas ...



Além disso, podemos definir as cores do próprio eixo Y colorYAxise as marcas - colorYMark:



RangeView - definir o intervalo de tempo usando o "mini-mapa".


A parte mais comovente de nossa interface do usuário é definir o intervalo de tempo ( lowerBound, upperBound) para exibir o "conjunto de gráficos":



RangeView- é meio que mini - mapdestacar uma seção de tempo específica para fins de uma consideração mais detalhada do "conjunto de gráficos" em outros Views.

Como nos anteriores View, os dados iniciais para RangeViewsão:



  • o próprio "conjunto de gráficos" chart: LineSet(valores Y),
  • altura height "mini-map" RangeView,
  • largura widthRange "mini-map" RangeView,
  • recuo indent "mini-map" RangeView.

Diferentemente dos outros discutidos acima Views, devemos alterar o DragGestureintervalo de tempo ( lowerBound, upperBound) com um gesto e ver imediatamente sua alteração, para que o intervalo de tempo definido pelo usuário ( lowerBound, upperBound) com o qual trabalharemos seja armazenado em uma variável variável @EnvironmentObject var userData: UserData:



qualquer alteração na variável var userDatalevará a redesenhar todos Viewsdependentes dele.

O personagem principal RangeViewé uma “janela” transparente, cuja posição e tamanho são regulados pelo usuário com um gesto DragGesture:

1. se usarmos o gesto dentro de uma “janela” transparente, a POSIÇÃO da “janela” Xmuda e seu tamanho não muda:



2. se usarmos um gesto na parte escura da esquerda, somente a FRONDA ESQUERDA da “janela” muda lowerBound, permitindo diminuir ou aumentar a largura da “janela” transparente:



3. se usarmos um gesto na parte escura da direita, somente a FRONDA DIREITA da “janela” muda upperBound, permitindo diminuir ou aumentar a largura da “janela” transparente:



RangeViewconsiste em 3 elementos básicos muito simples: dois retângulos Rectangle ()e uma imagem Image, cujas bordas são determinadas pelas propriedades lowerBounde a upperBoundpartir de @EnvironmentObject var userData: UserDatae são ajustadas usando gestos DragGesture:



“Sobrepomos” ( overlay) o familiar desta construção ( ) -nos GraphsForChartViewpara "gráfico" de um determinado "conjunto de gráficos" chart:



Isso nos permitirá monitorar quanto dos "gráficos" entra na "janela".

Qualquer mudança na "janela" transparente (ele é movido inteiramente ou mudança de fronteiras) é uma conseqüência de mudanças nas propriedades lowerBounde upperBoundem userData nas funções de onChangedprocessamento de sinal DragGesturenas duas caixas Rectangle ()e imagem Image...



Isto é, como já sabemos, levará automaticamente para redesenhar o outro Views(neste caso, “Gráficos”, eixo X com marcas, eixo Y com marcas e indicador c hartView):



Como o nosso Viewcontém uma variável @EnvironmentObject userData: UserData, para visualizações Previews, devemos definir seu valor inicial usando .environmentObject (UserData()):



CheckMarksView - "ocultando" e mostrando "gráficos".


CheckMarksViewé uma pilha horizontal HStackcom uma linha checkBoxespara alternar as propriedades de isHiddencada "Gráficos" individuais no "conjunto de gráficos" chart:



CheckBoxem nosso projeto, ele pode ser implementado usando um botão regular Buttone chamado CheckButton, ou usando um botão de simulação SimulatedButton.



O botão Buttonteve que ser imitado porque, ao colocar vários desses botões no Listlocalizado mais alto na hierarquia, eles "recusam" a funcionar corretamente. Este é um bug de longa data que ficou preso no Xcode 11 desde o beta 1 até a versão atual . A versão atual do aplicativo usa um botão simulado SimulatedButton.

O botão simulado SimulatedButtone o botão realCheckButtonuse a mesma coisa Viewpara a sua "aparência" - CheckBoxView. Isso HStackcontém Texe Image:



Observe que o parâmetro de inicialização CheckBoxViewé uma @Bindingvariável var line: Line. A propriedade isHiddendesta variável define a "aparência" CheckBoView:




Ao usar CheckBoViewin SimulatedButtone in, CheckButtonvocê deve usar o sinal $para linedurante a inicialização:




A propriedade da isHiddenvariável é lineativada SimulatedButtoncom onTapGesture...



... e in CheckButton- com o actionbotão usual Button:



Observe que o parâmetro de inicialização para SimulatedButtone CheckButtontambém é@Bindingvariável var line: Line. Portanto, seu uso deve ser aplicada $à CheckMarksViewvariável de comutação userData.charts[self.chartIndex].lines[self.lineIndex(line: line)].isHidden, que é armazenado em uma variável global variável @EnvironmentObject var userData:



Mantivemos não utilizado no projeto é actualmente CheckButtono caso, se de repente você Applevai corrigir este erro. Além disso, você pode tentar usar CheckButtonem CheckMarksViewvez SimulatedButtone certifique-se de que ele não funciona no caso da composição do conjunto, "um conjunto de gráficos ChartViewusando Listno ListChartsView.

Como o nosso Viewcontém uma variável @EnvironmentObject var userData: UserData, para visualizações Previews, devemos definir seu valor inicial com .environmentObject(UserData()):



Combinação de vários Views.


SwiftUI- isso é principalmente uma combinação de vários pequenos Viewsem grandes e grandes Viewsem muito grandes etc., como no jogo Lego. Como SwiftUIhá uma variedade de meios, tais combinação Views:

  • uma pilha vertical VStack,
  • pilha horizontal HStack,
  • "Profundidade" da pilha ZStack,
  • grupo Group,
  • ScrollView ,
  • lista List,
  • formar Form,
  • recipiente de marcador TabView
  • etc.

Começamos nossa combinação com a mais simples GraphsViewForChart, que fornece o GraphsForChartAXIS Y "sem rosto" "conjunto de gráficos" e um indicador movendo-se ao longo do eixo X usando a pilha "profunda" ZStack:



adicionamos um contêiner ao Previewsnosso novo GraphsViewForChartcontêiner NavigationViewpara exibi-lo no Darkmodo usando um modificador .collorScheme(.dark).

Continuamos a combinação e anexamos ao "conjunto de gráficos" obtido acima com o AXIS Y e um indicador, o AXIS X na forma de uma "linha de corrida", além de controles: o intervalo de tempo do "mini-mapa" RangeViewe os comutadores de CheckMarksViewexibição de "Gráficos".

Como resultado, obtemos o indicado acima ChartView, que exibe um "conjunto de gráficos" e permite controlar sua exibição no eixo do tempo:



Nesse caso, executamos a combinação usando a pilha vertical VStack:



Agora, consideraremos 3 opções para combinar o conjunto de "ChartView Charts" já recebidos:

  1. "Tabela rolável" List,
  2. pilha horizontal HStackcom efeito 3D,
  3. ZStack "cartões" sobrepostos

Uma “tabela rolável” éListChartsView organizada usando uma lista List: Uma



pilha horizontal com efeito 3D é organizada usando uma ScrollViewpilha horizontal HStacke uma lista no formato ForEach:



Nesta exibição, todos os meios de interação do usuário funcionam completamente: movendo-se na linha do tempo e alterando os mini- mapbotões “escala” , indicador e ocultar "Gráficos".

ZStack sobrepostos "cartões".


Primeiro criamos CardViewpara o “mapa” - este é um “conjunto de gráficos” com os EIXOS X e Y, mas sem controles: sem um “mini-mapa” e sem botões para controlar a aparência / ocultação de gráficos. CardViewmuito parecido com ChartView, mas como vamos sobrepor as "cartas" umas sobre as outras, precisamos que elas sejam opacas.Para isso, usamos uma ZStackcor adicional para ser colocada no "fundo" cardBackgroundColor. Além disso, vamos fazer para "mapa" uma caixa com cantos arredondados:



superposição do "mapa" organizado por uma pilha VStack, ZStackea lista na forma ForEach:



Mas vamos impor uns sobre os outros não é apenas um "cartão" e "3D-macshtabiruemye" cartões CardViewScalable, cujo tamanho diminui com o aumento do índiceindexChate eles mudam um pouco na vertical.

A ordem dos "cartões escaláveis ​​em 3D" pode ser alterada usando a sequência ( sequenced) de gestos LongPressGesturee DragGesture, que atua apenas no "cartão" mais alto com indexChat == 0:



Você pode clicar em ( LongPress) no "cartão" superior com um "conjunto de gráficos" e puxá-lo Dragpara baixo ( ) longe o suficiente para olhar para a próxima carta e, se você continuar a arrastá-la para baixo, ela "vai" para o último lugar ZStacke a próxima "carta" aparece:



Além disso, para a "carta" superior podemos aplicar TapGesture, o que agirá juntamente com gestos LongPressGesturee uma DragGesture:



Tapum gesto mostrará o modal "conjunto de gráficos" ChartViewcom e gestão ementami RangeVieweCheckMarksView :



Pedido TabViewde combinação em uma tela de todas as 3 variantes da composição "conjunto de gráficos" ChartView.





Temos 3 marcadores com uma imagem Imagee um texto Text, VStacknão é necessária uma pilha vertical para sua apresentação conjunta.

Eles correspondem aos nossos três métodos de combinação de "conjuntos de gráficos" ChartViews:

  1. "Tabela rolável" ListChartViews,
  2. pilha horizontal com efeito 3D HStackChartViews,
  3. O ZStack sobrepôs "cartões" OverlayCardsViews.

Todos os elementos da interação do usuário: movendo-se ao longo da linha do tempo e alterando a “escala” com a ajuda mini - map, indicador e botões para ocultar os “Gráficos”. funcione totalmente em todos os 3 casos.

O código está no Github .

SwiftUI ...


Você deve estar familiarizado com tutoriais em vídeo, livros e blogs:

Mang do Para , Deixa Que a criar o aplicativo , bem como uma descrição de algumas aplicações SwiftUI ,
- livre livro «SwiftUI por exemplo» e vídeo www.hackingwithswift.com/quick-start/swiftui
- livro de pagamento mas metade dele pode ser baixado gratuitamente www.bigmountainstudio.com/swiftui-views-book
- curso de 100 dias com SwiftUI www.hackingwithswift.com/articles/201/start-the-100-days-of-swiftui , que começa agora e terminará em 31 de dezembro de 2019,
- coisas impressionantes no SwiftUI são feitas no swiftui-lab.com
- Majid blog ,
- no pointFree.cowww.pointfree.co A “maratona” de posts sobre o uso de redutores no SwiftUI (super interessante)
é um ótimo aplicativo MovieSwiftUI que emprestou algumas idéias.

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


All Articles