SwiftUI para la última tarea competitiva de Telegram Charts (marzo de 2019): todo es simple



Comenzaré con el comentario de que la aplicación que se trata en este artículo requiere Xcode 11 y MacOS Catalina si desea usar Live Previews , y Mojave si usa el simulador. El código de la aplicación está en Github .

Este año en WWDC 2019 , Apple anunció SwiftUI , una nueva forma declarativa de construir una interfaz de usuario (UI) en todos Apple dispositivos Apple . Esto es casi una desviación completa del UIKit habitual, y yo, como muchos otros desarrolladores, realmente quería ver esta nueva herramienta en acción.

Este artículo presenta la experiencia de resolver con SwiftUI un problema cuyo código dentro de UIKit incomparablemente más complejo y, en mi opinión, no se puede UIKit de una manera legible.

La tarea está relacionada con el último concurso de Telegram para desarrolladores de Android , iOS y JS , que se realizó del 10 al 24 de marzo de 2019. En esta competencia, se propuso una tarea simple para mostrar gráficamente la intensidad de uso de un determinado recurso en Internet dependiendo del tiempo basado en datos JSON . Como desarrollador de iOS , debe usar Swift para enviar código escrito desde cero a la competencia sin usar bibliotecas especializadas extrañas para trazar.

Esta tarea requería habilidades para trabajar con las capacidades gráficas y de animación de iOS: Core Graphics , Core Animation , Metal , OpenGL ES . Algunas de estas herramientas son herramientas de programación de bajo nivel, no orientadas a objetos. Esencialmente, en iOS no había plantillas aceptables para resolver tareas gráficas aparentemente tan ligeras a primera vista. Por lo tanto, cada concursante inventó su propio animador ( Render ) basado en Metal , CALayers , OpenGL , CADisplayLink . Esto generó toneladas de código del cual no fue posible tomar prestado y desarrollar nada, ya que estos son trabajos puramente "con derechos de autor" que solo los autores realmente pueden desarrollar. Sin embargo, esto no debería ser así.

Y a principios de junio en WWDC 2019 , aparece SwifUI , un nuevo framework desarrollado por Apple , escrito en Swift y diseñado para describir declarativamente la interfaz de usuario ( UI ) en el código. Usted determina qué subviews muestran en su View , qué datos hacen que estas subviews cambien, qué modificadores necesita aplicarles, para colocarlas en el lugar correcto, para tener el tamaño y estilo correctos. Un elemento igualmente importante de SwiftUI es el control del flujo de datos modificables por el usuario, que a su vez actualiza la UI .

En este artículo quiero mostrar cómo la tarea misma del concurso Telegram en SwiftUI se resuelve rápida y fácilmente. Además, este es un proceso muy emocionante.

Tarea


La aplicación competitiva debería mostrar simultáneamente 5 "conjuntos de gráficos" en la pantalla utilizando los datos proporcionados por Telegram . Para un "conjunto de gráficos", la UI siguiente:



En la parte superior hay una "zona de gráfico" con una escala común a lo largo del eje Y normal con marcas y líneas de cuadrícula horizontales. Debajo de eso hay una línea progresiva con marcas de tiempo a lo largo del eje X como fechas.

Aún más bajo está el llamado "mini mapa" (como en Xcode 11 ), es decir, una "ventana" transparente que define esa parte del período de tiempo de nuestros "Gráficos", que se presenta con más detalle en la "zona de Gráficos" superior. Este "mini mapa" no solo se puede mover a lo largo del eje X , sino que también se puede cambiar su ancho, lo que afecta la escala de tiempo en el "área de Gráficos".

Con la ayuda de checkboxs de checkboxs pintadas con los colores de "Gráficos" y provistos de sus nombres, puede negarse a mostrar los "Gráficos" correspondientes a este color en la "zona de Gráficos".

Hay muchos de estos "conjuntos de gráficos", en nuestro ejemplo de prueba hay 5 de ellos, por ejemplo, y todos deben ubicarse en una pantalla.

En la UI diseñada con SwiftUI no es necesario un botón para cambiar entre los modos Dark y Light , esto ya está integrado en SwiftUI . Además, SwiftUI muchas más opciones para combinar "conjuntos de gráficos" (es decir, los conjuntos de pantallas presentados anteriormente) que simplemente desplazarse hacia abajo en una tabla, y veremos algunas de estas opciones muy interesantes.

Pero primero, centrémonos en mostrar un "conjunto de SwiftUI " para el que crearemos un ChartView :



SwiftUI permite crear y probar una UI compleja en piezas pequeñas, y luego es muy fácil ensamblar estas piezas en un rompecabezas. Nosotros lo haremos. Nuestro ChartView muy bien en estas pequeñas piezas:

  • GraphsForChart : estos son los gráficos en sí, creados para un "conjunto de gráficos" específico. Los "gráficos" se muestran para el rango de tiempo controlado por el usuario utilizando el "mini mapa" RangeView , que se presentará a continuación.
  • YTickerView es el eje Y con elevaciones y la cuadrícula horizontal correspondiente.
  • IndicatorView es un indicador controlado horizontalmente por el usuario que le permite ver los valores de "Gráficos" y el tiempo para la posición del indicador correspondiente en el eje de tiempo en el eje X
  • TickerView : "línea progresiva" que muestra marcas de tiempo en el eje X como fechas,
  • RangeView : una "ventana" temporal, personalizable por el usuario mediante gestos, para establecer el intervalo de tiempo para "Gráficos",
  • CheckMarksView : contiene “botones” coloreados en los colores de “Gráficos” y le permite controlar la presencia de “ ChartView ” en ChartView .

ChartView usuario puede interactuar con ChartView de tres maneras:

1. controle el "mini mapa" con el gesto DragGesture : puede desplazar la "ventana" temporal hacia la derecha e izquierda y disminuir / aumentar su tamaño:



2. mueva el indicador en dirección horizontal, mostrando los valores de los "Gráficos" en un punto fijo en el tiempo:



3. ocultar / mostrar ciertos "Gráficos" usando botones de colores en los "Gráficos" y ubicados en la parte inferior de la ChartView de ChartView :



Podemos combinar varios "Conjuntos de gráficos" (tenemos 5 de ellos en los datos de prueba) de diferentes maneras, por ejemplo, colocándolos a todos simultáneamente en una pantalla usando la List Lista (como una tabla desplazable hacia arriba y hacia abajo):



o usando ScrollView y una pila horizontal de HStack con un efecto 3D:



... o en forma de una ZStack "cartas" superpuestas entre sí, cuyo orden se puede cambiar: la "carta" superior con "" un conjunto de Gráficos "se puede tirar lo suficiente como para mirar la siguiente carta, y si continúa arrastrándola hacia abajo, entonces" va "al último lugar en ZStack , y esta próxima" tarjeta "" sigue adelante ":



En estas UI complejas (una "tabla desplazable", una pila horizontal con un efecto 3D , una ZStack "cartas" superpuestas entre sí), todos los medios de interacción con el usuario funcionan completamente: moverse a lo largo de la línea de tiempo y cambiar la "escala" del mini - map , el indicador y los botones de ocultar "Gráficos".

Además, consideraremos en detalle el diseño de esta UI de UI utilizando SwiftUI , desde elementos simples hasta sus composiciones más complejas. Pero primero, comprendamos la estructura de datos que tenemos.

Entonces, la solución a nuestro problema se dividió en varias etapas:

  • Descargue datos de un archivo JSON y preséntelos en un conveniente formato "interno"
  • Crear UI de UI para un "conjunto de gráficos"
  • Combina varios "conjuntos de gráficos"

Descargar datos


A nuestra disposición, Telegram proporcionó datos JSON que contienen varios "conjuntos de gráficos". Cada "conjunto de chart " individual de un chart contiene varios "Gráficos" (o "Líneas") de chart.columns de chart.columns . Cada "Gráfico" ("Líneas") tiene una marca en la posición 0 - "x" , "y0" , "y1" , "y2" , "y3" , seguido de los valores de tiempo en el eje X ("x") , o los valores de "Gráficos" ("Líneas") ( "y0" , "y1" , "y2" , "y3" ) en el eje Y :



La presencia de todas las "Líneas" en el "conjunto de gráficos" es opcional. Los valores para la "columna" x son marcas de tiempo UNIX en milisegundos.

Además, cada "conjunto de chart " individual del chart se suministra con colores chart.colors en el formato de 6 dígitos hexadecimales (por ejemplo, "#AAAAAA") y chart.names .

Para construir el modelo de datos ubicado en el archivo JSON , utilicé el excelente servicio de tipo rápido . En este sitio, inserta un fragmento de texto de un archivo JSON y especifica el lenguaje de programación ( Swift ), el nombre de la estructura ( Chart ), que se formará después del "análisis" de estos datos JSON y eso es todo.

Se genera un código en la parte central de la pantalla, que copiamos en nuestra aplicación en un archivo separado llamado Chart.swift . Aquí es donde colocaremos el modelo de datos en formato JSON. Utilizando el cargador de datos del archivo JSON al modelo prestado de las demostraciones Generic SwiftUI , obtuve una serie de columns: [ChartElement] , que es una colección de "conjuntos de columns: [ChartElement] " en el formato de Telegram .

La ChartElement datos de ChartElement , que contiene matrices de elementos heterogéneos, no es muy adecuada para el trabajo interactivo intensivo con gráficos, además, las marcas de tiempo se presentan en formato UNIX en milisegundos (por ejemplo, 1542412800000, 1542499200000, 1542585600000, 1542672000000 ) y colores en 6 formatos hexadecimales. dígitos (por ejemplo, "#AAAAAA" ).

Por lo tanto, dentro de nuestra aplicación utilizaremos los mismos datos, pero en un formato "interno" y bastante simple [LinesSet] . La matriz [LinesSet] es una colección de LinesSet "Sets de LinesSet ", cada uno de los cuales contiene xTime tiempo xTime en el formato "Feb 12, 2019" (eje X ) y varias lines "Gráficos" (eje Y ):



Se presentan los datos para cada gráfico de líneas (línea)

  • una serie de points: [Int] enteros points: [Int] ,
  • titulado "Gráficos" title: String ,
  • tipo "Gráficos" type: String? ,
  • color : UIColor en el formato UIColor ,
  • número de puntos countY: Int .

Además, cualquier "Gráfico" se puede ocultar o mostrar dependiendo del valor de isHidden: Bool . Los upperBound lowerBound y upperBound ajustar el rango de tiempo toman valores de 0 a 1 y muestran no solo el tamaño de la ventana de tiempo del "mini mapa" ( upperBound - lowerBound ), sino también su ubicación en el eje de tiempo X :



Las estructuras de datos JSON [ChartElement] y las estructuras de datos de las LinesSet "internas" de LinesSet y Line están en el archivo Chart.swift . El código para cargar datos JSON y convertirlos en una estructura interna se encuentra en el archivo Data.swift . Los detalles sobre estas transformaciones se pueden encontrar aquí .

Como resultado, recibimos datos sobre los "conjuntos de gráficos" en el formato interno como una matriz de chartsData .



Este es nuestro datos, pero para trabajar en SwiftUI es necesario asegurarse de que cualquier cambio realizado por el usuario en la matriz de datos de chartsData (cambiar la "ventana" temporal, ocultar / mostrar los "Gráficos") conduzca a actualizaciones automáticas de nuestras Views .

Crearemos @EnvironmentObject . Esto nos permitirá usar el datos donde sea necesario y, además, actualizar automáticamente nuestras Views si los datos cambian. Esto es algo así como Singleton o datos globales.

@EnvironmentObject requiere que @EnvironmentObject una final class UserData , que se encuentra en el archivo UserData.swift , almacena los datos de chartsData e implementa el protocolo ObservableObject :



La presencia de "wrappers" @Published le permitirá publicar "noticias" de que estas propiedades de los charts de la clase UserData han cambiado, de modo que cualquier Views "suscrita a esta noticia" en SwiftUI podrá seleccionar automáticamente nuevos datos y actualizaciones.

Recuerde que en la propiedad de charts , los valores de isHidden pueden cambiar para cualquier " isHidden " (le permiten ocultar o mostrar estos "Gráficos"), así como el lowerBound inferior inferior y el upperBound superior superior upperBound intervalo de tiempo para cada "conjunto de Gráficos" individual.

Queremos usar la propiedad de charts de la clase UserData toda nuestra aplicación y no tenemos que sincronizarlos manualmente con la UI usuario gracias a @EnvironmentObject .

Para hacer esto, al iniciar la aplicación, debemos crear una instancia de la clase UserData () para que posteriormente podamos acceder a ella desde cualquier lugar de nuestra aplicación. Haremos esto en el archivo SceneDelegate.swift dentro de la scene (_ : , willConnectTo: , options: ) método. Aquí es donde se crea y lanza nuestro ContentView , y es aquí donde debemos pasar el ContentView cualquier @EnvironmentObject que @EnvironmentObject para que SwiftUI pueda ponerlos a disposición de cualquier otra View :



Ahora, en cualquier View para acceder a los datos @Published de la clase UserData , necesitamos crear la variable var utilizando el contenedor @EnvironmentObject . Por ejemplo, al establecer el rango de tiempo en RangeView creamos la variable var userData con el UserData TYPE:



Por lo tanto, tan pronto como hayamos implementado algún @EnvironmentObject en el "entorno" de la aplicación, podemos comenzar a usarlo de inmediato en el nivel más alto o en el décimo nivel a continuación, no importa. Pero lo más importante, cada vez que una View cambia el "entorno", todas las Views que tienen este @EnvironmentObject se @EnvironmentObject automáticamente, asegurando así la sincronización con los datos.

Pasemos a diseñar la interfaz de usuario ( UI ).

Interfaz de usuario (UI) para un "conjunto de gráficos"


SwiftUI ofrece una tecnología compuesta para crear SwiftUI de SwiftUI partir de muchas Views pequeñas, y ya hemos visto que nuestra aplicación encaja muy bien con esta tecnología, ya que se divide en piezas pequeñas: el conjunto " ChartView Charts", " GraphsForChart Charts", Y -axis YTickerView - YTickerView , el valor del indicador controlado por el usuario de "Charts" IndicatorView , " TickerView " TickerView con TickerView de tiempo en el eje X , RangeView "ventana de tiempo" controlada por el usuario, marcas al ocultar / mostrar "Charts" CheckMarksView . No solo podemos crear todas estas Views independientemente una de la otra, sino también probar de inmediato en Xcode 11 usando Vistas Previews ( Previews preliminares "en vivo") en los datos de prueba. Se sorprenderá de lo simple que es el código para crearlos desde otras Views más básicas.

GraphView - "Gráfico" ("Línea")


La primera View , con la que comenzaremos, es en realidad el "Gráfico" en sí (o "Línea"). Lo llamaremos GraphView :



Crear un GraphView , como siempre, comienza con la creación de un nuevo archivo en Xcode 11 usando el menú FileNewFile :



Luego seleccionamos el TIPO deseado del archivo: este es el archivo SwiftUI :



... asigne el nombre "GraphView" a nuestra View e indique su ubicación:



Haga clic en el botón "Create" y obtenga una View estándar con Text ( "Hello World!") En el centro de la pantalla:



Nuestra tarea es reemplazar el Text ("Hello World!") con "Gráfico", pero primero, veamos qué datos iniciales tenemos para crear el "Gráfico":

  • tenemos los valores de line.points "Graphics" line: Line ,
  • rango de tiempo rangeTime , que es un rango de índices Range xTime Range tiempo xTime en el eje X,
  • rango de valores rangeY: Range "Gráficos" para el rangeY: Range Y,
  • grosor de la línea de trazo "Gráficos" lineWidth línea.

Agregue estas propiedades a la estructura GraphView :



Si queremos usar para nuestras vistas Previews (vistas previas) de "Gráficos", que solo son posibles para MacOS Catalyna , entonces debemos iniciar un GraphView con el rango de índices rangeTime y los datos de line de los "Gráficos" en sí:



Ya tenemos los datos de prueba de chartsData que obtuvimos del archivo JSON chart.json , y los usamos para Previews .

En nuestro caso, este será el primer "conjunto de chartsData[0] " chartsData[0] y el primer "Gráfico" en este conjunto de chartsData[0].lines[0] , que proporcionaremos GraphView como parámetro de line .

Usaremos el rango completo de índices 0..<(chartsData[0].xTime.count - 1) como el intervalo de tiempo rangeTime .
Los lineWidth rangeY y lineWidth se pueden establecer externamente o no, ya que ya tienen valores iniciales: rangeY es nil y lineWidth es 1 .

Hicimos intencionalmente un TYPE de la propiedad rangeY Optional con un TYPE, porque si rangeY no se establece externamente y rangeY = nil , entonces calculamos el mínimo minY y el máximo maxY "Gráficos" directamente a partir de los datos de line.points :



Este código se compila, pero aún tenemos una View estándar en la pantalla con el texto Text ("Hello World!") En el medio de la pantalla:



Porque en el body tenemos que reemplazar el texto Text ("Hello World!") Con Path , que construirá nuestro "Graph: points: line.points usando el addLines(_:) (casi como en Core Graphics )




Vamos a rodear el stroke (...) nuestra Path línea cuyo grosor es lineWidth , y el color de la línea del trazo corresponderá al color predeterminado (es decir, negro):



Podemos reemplazar el color negro de la línea de trazo con el color especificado en nuestro line.color "Color" particular de "Línea":



Para que nuestro "Gráfico" se coloque en rectángulos de cualquier tamaño, utilizamos el contenedor GeometryReader . En la documentación de Apple GeometryReader es una vista de "contenedor" que define su contenido en función de su propio tamaño de size y espacio de coordenadas. Esencialmente, GeometryReader es otra View ! ¡Porque casi TODO en SwiftUI es View ! GeometryReader le permitirá a USTED, a diferencia de otras Views acceder a información adicional útil que puede usar al diseñar su View personalizada.

Utilizamos los contenedores GeometryReader y Path para crear GraphView adaptable a cualquier tamaño. Y si miramos cuidadosamente nuestro código, veremos en el cierre de GeometryReader variable llamada geometry :



Esta variable tiene el GeometryProxy TYPE, que a su vez es una struct estructura con muchas "sorpresas":

 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 } 

De la definición de GeometryProxy , vemos que hay dos variables calculadas var size y var safeAreaInsets , un frame( in:) función frame( in:) y un subscript getter . Solo necesitábamos la variable de size para determinar el ancho de geometry.size.width y la altura de geometry.size.height área de dibujo "Gráficos".

Además, habilitamos nuestro "Gráfico" para animar usando el modificador de animation (.linear(duration: 0.6)) .



GraphView_Previews nos permite probar fácilmente cualquier "Gráfico" de cualquier "conjunto". A continuación se muestra el "Gráfico" del "conjunto de gráficos" con el índice 4: chartsData[4] e índice 0 "Gráficos" en este conjunto: chartsData[4].lines[0] :



Establecemos la height "Gráficos" en 400 usando el frame (height: 400) , el ancho sigue siendo el mismo que el ancho de la pantalla. Si no utilizáramos el frame (height: 400) , entonces el "Gráfico" ocuparía toda la pantalla.No especificamos un rango de valores rangeYy GraphViewusamos el valor nilpredeterminado, en este caso, el "Gráfico" toma sus valores mínimos y máximos en el intervalo de tiempo rangeTime:



aunque usamos un Pathmodificador para nuestro modelo animation (.linear(duration: 0.6)), no se producirá animación, por ejemplo, al cambiar el rango de rangeYvalores " Gráficos ". Un "gráfico" simplemente "saltará" de un valor de un rango rangeYa otro sin ninguna animación.

La razón es simple: enseñamos SwiftUIcómo dibujar un "Gráfico" para un rango específico rangeY, pero no enseñamos SwiftUIcómo reproducir un "Gráfico" varias veces con valores intermedios del rango rangeYentre el inicio y el final, y para eso enSwiftUIcumple con el protocolo Animatable.

Afortunadamente, si el suyo Viewes una "figura", es decir View, que implementa un protocolo Shape, entonces ya se ha implementado un protocolo para él Animatable. Esto significa que hay una propiedad calculada animatableDatacon la que podemos controlar el proceso de animación, pero de forma predeterminada se establece en EmptyAnimatableData, es decir, no se produce animación.

Para resolver el problema con la animación, primero debemos convertir nuestro "Gráfico" GraphViewen Shape. Es muy simple, solo necesitamos implementar la función func path (in rect:CGRect) -> Pathque esencialmente ya tenemos e indicar con la ayuda de la propiedad calculada animatableDataqué datos queremos animar:



Tenga en cuenta que el tema del control de animación es un tema avanzado enSwiftUIy puede obtener más información al respecto en el artículo "Animaciones avanzadas de SwiftUI - Parte 1: Rutas" . Podemos usar la

"figura" resultante Graphen una GraphViewNew"Gráfica" mucho más simple con animación:



verá que no necesitábamos GeometryReadernuestras nuevas "Gráficas" GraphViewNew, porque gracias al protocolo Shapenuestra "figura" Graphpodrá adaptarse a cualquier tamaño del padre View.

Naturalmente, Previewsobtuvimos el mismo resultado que en el caso con GraphView:



En las siguientes combinaciones, usaremos los GraphViewNewmismos "Gráficos" para mostrar los valores.

GraphsForChart - conjunto de "Gráficos" ("Líneas")


El propósito de este View- Mostrar todo "Gráficos" ( "líneas") de "fijó gráficos" charten un intervalo de tiempo predeterminado rangeTimecon un eje común Y, la anchura de las "líneas" es igual a lineWidth:



Al igual que con GraphViewy GraphViewNewvamos a crear GraphsForChartun nuevo archivo GraphsForChart.swift, y definir los datos de entrada para "Conjunto de gráficos":

  • el "conjunto de gráficos" en sí chart: LineSet(valores en Y),
  • rango rangeTime: Range( X) de índices de las marcas de tiempo de "Gráficos",
  • grosor de trazo de línea gráfica lineWidth

El rango de valores rangeY: Rangepara el "conjunto de gráficos" ( Y) se calcula como la unión de los rangos de los isHidden = false"Gráficos" no ocultos ( ) individuales incluidos en este "conjunto":



Para esto, utilizamos la función rangeOfRanges: Mostramos



todos los Gráficos NO ocultos ( isHidden = false) en ZStackla construcción ForEach, dando a cada "Gráfico" la posibilidad de aparecer en la pantalla y salir de la pantalla "usando el modificador" mover " transition(.move(edge: .top)):



Gracias a este modificador, el proceso de ocultar y devolver los" Gráficos " ChartViewa la pantalla tendrá lugar en la pantalla con animación y dejará en claro al usuario por qué ha cambiado la escala Y.

Uso drawingGroup()significa usoMetalpara dibujar formas gráficas. En nuestros datos de prueba y en el simulador, no sentirá la diferencia en la velocidad de dibujar con Metaly Metal, pero si reproduce muchos gráficos bastante voluminosos en alguno iPhone, notará esta diferencia. Para una introducción más detallada, cuándo usarlo drawingGroup(), puede ver el artículo "Animaciones avanzadas de SwiftUI - Parte 1: Rutas" o ver la sesión de video 237 WWDC 2019 ( Creación de vistas personalizadas con SwiftUI ).

Como en el caso de las GraphViewNewpruebas GraphsForChartcon vistas previas, Previewspodemos establecer cualquier "conjunto de gráficos", por ejemplo, con un índice 0:



IndicatorView - Indicador "Gráficos" movido horizontalmente.


Este indicador le permite obtener los valores exactos de "Gráficos" y el tiempo para el punto correspondiente en el tiempo X:



El indicador se crea para un "conjunto de Gráficos" específico charty consiste en moverse a lo largo de la XLÍNEA vertical con MARCAS en forma de "círculos" en lugar de los valores de "Gráficos". Un pequeño "CARTEL" se adjunta a la parte superior de esta línea vertical, que contiene los valores numéricos de los "Gráficos" y el tiempo.



El indicador se desliza por el usuario usando un gesto DragGesture:



utilizamos la llamada ejecución de gesto "incremental". En lugar de una distancia continua desde el punto de partida value.translation.width, onChangedrecibiremos constantemente la distancia desde el lugar donde estuvimos la última vez que realizamos el gesto en el controlador :value.translation.width - self.prevTranslation. Esto nos proporcionará un movimiento suave del indicador.

Para probar el indicador IndicatorViewcon la ayuda de un Previews"conjunto de Gráficos" dado, chartpodemos atraer la Viewconstrucción de "Gráficos" GraphsForChart:



podemos establecer cualquier rango de tiempo, pero coordinado entre sí, rangeTimetanto para el indicador IndicatorViewcomo para los "Gráficos" GraphsForChart. Esto nos permitirá asegurarnos de que los "círculos" que indican los valores de los "Gráficos" estén en los lugares correctos.

TickerView- Xcon marcas


Hasta ahora, nuestros "Gráficos" están despersonalizados en el sentido de que NO X Ytienen las escalas y marcas apropiadas. Dibujemos Xcon marcas TickerMarkViewde tiempo . marca Sami TickerMarkViewson muy simples Viewpila vertical VStacken la que están dispuestos Pathy Text:



El conjunto de marcas en el eje de tiempo para un cierto "gráficos conjunto" chart : LineSetformados TickerViewde acuerdo con el intervalo de tiempo seleccionado por el usuario rangeTimey la cantidad aproximada de las marcas estimatedMarksNumber, que deben estar en el campo de visión del usuario:



Para disposición Marcas de tiempo "en ejecución" usamos una ScrollViewpila horizontalHStack, que cambiará a medida que cambie el rango de tiempo rangeTime.

En TickerView, formamos un paso stepcon el que aparecen las marcas de tiempo TimeMarkView, en función de un rango de tiempo rangeTimey un ancho de pantalla determinados widthRange...



... y luego seleccionamos marcas de tiempo en incrementos stepde la matriz chart.xTimemediante índices indexes.

En realidad X- línea horizontal - que impone overlay...



... en la pila horizontal HStack, con marcas de tiempo TimeMarkView, que estamos impulsando con la ayuda offset:



Además, podemos establecer el color en sí mismo X- colorXAxisy marcas - colorXMark:



YTickerView- YCon marcas y una cuadrícula.


Este Viewdibuja Ycon marcas digitales YMarkView. propios marcadores YMarkViewson muy simples Viewpila vertical VStacken que están dispuestos Path(la línea horizontal) y Textcon un número:



El conjunto de marcas en Ypara un cierto "conjunto de gráficos" chartse forma YTickerView. El rango de valores se rangeYcalcula como la unión de los rangos de valores de todos los "Gráficos" incluidos en este "conjunto de Gráficos" utilizando la función rangeOfRanges. Número aproximado de marcas en el eje Y se define mediante parámetro estimatedMarksNumber:



En YTickerViewsupervisamos el rango de cambio de los valores "gráficos" rangeY. En realidad, el eje Y, la línea vertical, imponemos overlaya nuestras marcas ...



Además, podemos establecer los colores del propio eje Y colorYAxisy las marcas colorYMark:



RangeView - establecer el rango de tiempo usando el "mini-mapa".


La parte más conmovedora de nuestra interfaz de usuario es establecer el rango de tiempo ( lowerBound, upperBound) para mostrar el "conjunto de Gráficos":



RangeView- es como mini - mapresaltar una sección de tiempo específica con el propósito de considerar más detalladamente el "conjunto de Gráficos" en otros Views.

Como en los anteriores View, los datos iniciales RangeViewson:



  • el "conjunto de gráficos" en sí chart: LineSet(valores Y),
  • de altura height "mini-map" RangeView,
  • anchura widthRange "mini-map" RangeView,
  • Guión indent "mini-map" RangeView.

A diferencia de los otros discutidos anteriormente Views, debemos cambiar el DragGesturerango de tiempo ( lowerBound, upperBound) con un gesto e inmediatamente ver su cambio, por lo que el rango de tiempo definido por el usuario ( lowerBound, upperBound) con el que trabajaremos se almacena en una variable variable @EnvironmentObject var userData: UserData:



cualquier cambio en la variable var userDataconducirá a redibujando a todos los Viewsdependientes de él.

El personaje principal en RangeViewes una "ventana" transparente, cuya posición y tamaño son controlados por el usuario con un gesto DragGesture:

1. si usamos el gesto dentro de una "ventana" transparente, la POSICIÓN de la "ventana" cambia X, y su tamaño no cambia:



2. si usamos un gesto en la parte oscura a la izquierda, solo cambia la FRONTERA IZQUIERDA de la "ventana" lowerBound, lo que nos permite disminuir o aumentar el ancho de la "ventana" transparente:



3. si usamos un gesto en la parte oscura a la derecha, solo cambia la FRONTERA DERECHA de la "ventana" upperBound, lo que le permite disminuir o aumentar el ancho de la "ventana" transparente:



RangeViewconsta de 3 elementos básicos muy simples: dos rectángulos Rectangle ()y una imagen Image, cuyos bordes están determinados por las propiedades lowerBoundy upperBounddesde @EnvironmentObject var userData: UserDatay se ajustan mediante gestos DragGesture:



"superponemos" ( overlay) lo familiar a esta construcción ( ) nosotros GraphsForChartViewcon "Gráficos" de un "conjunto de Gráficos" dado chart:



Esto nos permitirá controlar la cantidad de "Gráficos" que ingresa a la "ventana".

Cualquier cambio en la "ventana" transparente (que se mueve por completo o cambio de fronteras) es una consecuencia de los cambios en las propiedades lowerBoundy upperBounden userData en las funciones de onChangedprocesamiento de señal DragGestureen las dos cajas Rectangle ()y foto Image...



Se trata, como ya sabemos, dará lugar automáticamente a volver a dibujar el otro Views(en este caso, “Gráficos”, eje X con marcas, eje Y con marcas e indicador c hartView):



dado que el nuestro Viewcontiene una variable @EnvironmentObject userData: UserData, para las vistas previas Previews, debemos establecer su valor inicial usando .environmentObject (UserData()):



CheckMarksView - "escondiéndose" y mostrando "Gráficos".


CheckMarksViewes una pila horizontal HStackcon una fila checkBoxespara cambiar las propiedades de isHiddencada "Gráficos" individuales en el "conjunto de Gráficos" chart:



CheckBoxen nuestro proyecto se puede implementar usando un botón normal Buttony llamado CheckButton, o usando un botón de simulación SimulatedButton.



El botón Buttontuvo que ser imitado porque al colocar varios de estos botones en Listuno ubicado más arriba en la jerarquía, se "niegan" a funcionar correctamente. Este es un error de larga data que se ha atascado en Xcode 11 desde la versión beta 1 hasta la versión actual . La versión actual de la aplicación utiliza un botón simulado SimulatedButton.

Tanto el botón simulado SimulatedButtoncomo el botón realCheckButtonusar lo mismo Viewpara su "apariencia" - CheckBoxView. Esto HStackcontiene Texy Image:



Tenga en cuenta que el parámetro de inicialización CheckBoxViewes una @Bindingvariable var line: Line. La propiedad de isHiddenesta variable define la "apariencia" CheckBoView:




al usar CheckBoViewin SimulatedButtony in, CheckButtondebe usar el signo $para linedurante la inicialización:




la propiedad de la isHiddenvariable se linecambia SimulatedButtoncon onTapGesture...



... y en CheckButton- con el actionbotón habitual Button: tenga en



cuenta que el parámetro de inicialización para SimulatedButtony CheckButtontambién es@Bindingvariables var line: Line. Por lo tanto, su uso debe ser aplicado $a la CheckMarksViewvariable de conmutación userData.charts[self.chartIndex].lines[self.lineIndex(line: line)].isHidden, que se almacena en una variable global variables @EnvironmentObject var userData:



Hemos mantenido sin utilizar en el proyecto se encuentra actualmente CheckButtonen el caso, si de repente se Appleva a corregir este error. Además, puede intentar usar CheckButtonin en su CheckMarksViewlugar SimulatedButtony asegurarse de que no funciona para el caso de componer una multitud de "conjuntos de gráficos" ChartViewutilizando Listc ListChartsView.

Como el nuestro Viewcontiene una variable @EnvironmentObject var userData: UserData, para las vistas previas Previews, debemos establecer su valor inicial con .environmentObject(UserData()):



Combinación de varios Views.


SwiftUI- esto es principalmente una combinación de varios pequeños Viewsen grandes, y grandes Viewsen muy grandes, etc., como en un juego Lego. Como SwiftUIhay una gran variedad de medios tales combinación Views:

  • una pila vertical VStack,
  • pila horizontal HStack,
  • "Profundidad" de la pila ZStack,
  • grupo Group,
  • ScrollView ,
  • lista List,
  • formar Form,
  • contenedor de marcadores TabView
  • etc.

Comenzamos nuestra combinación con la más simple GraphsViewForChart, que proporciona el "conjunto de gráficos" sin rostro " GraphsForChartAXIS Y" y un indicador que se mueve a lo largo del eje X usando la pila "profunda" ZStack:



agregamos un contenedor a Previewsnuestro nuevo GraphsViewForChartcontenedor NavigationViewpara mostrarlo en Darkmodo usando un modificador .collorScheme(.dark).

Continuamos la combinación y adjuntamos al "conjunto de gráficos" obtenido anteriormente con AXIS Y y un indicador, AXIS X en forma de una "línea en ejecución", así como controles: el rango de tiempo del "mini-mapa" RangeViewy los interruptores de CheckMarksViewvisualización de "Gráficos".

Como resultado, obtenemos el indicado anteriormente ChartView, que muestra un "conjunto de gráficos" y le permite controlar su visualización en el eje de tiempo:



En este caso, realizamos la combinación usando la pila vertical VStack:



ahora consideraremos 3 opciones para combinar el conjunto de "Conjuntos de gráficos" ChartView ya recibidos:

  1. "Mesa desplazable" List,
  2. pila horizontal HStackcon efecto 3D,
  3. ZStack "tarjetas" superpuestas

Una "tabla desplazable" seListChartsView organiza usando una lista List: una



pila horizontal con un efecto 3D se organiza usando una ScrollViewpila horizontal HStacky una lista en la forma ForEach:



en esta vista, todos los medios de interacción del usuario funcionan completamente: moverse a lo largo de la línea de tiempo y cambiar la "escala" mini- map, el indicador y los botones de ocultar "Gráficos".

ZStack "tarjetas" superpuestas.


Primero creamos CardViewpara el "mapa": este es un "conjunto de gráficos" con AXIS X e Y, pero sin controles: sin un "mini mapa" y sin botones para controlar la apariencia / ocultación de los gráficos. CardViewmuy similar a ChartView, pero como vamos a superponer "tarjetas" una encima de la otra, necesitamos que sean opacas. Para este fin, usamos un ZStackcolor adicional para colocar en el "fondo" cardBackgroundColor. Además, vamos a hacer para "mapa" de una caja con bordes redondeados:



superposición del "mapa" organizado por una pila VStack, ZStacky la lista en la forma ForEach:



Pero vamos a imponer a la otra no es sólo una "tarjeta" y "3D-macshtabiruemye" tarjetas CardViewScalable, cuyo tamaño disminuye al aumentar el índiceindexChaty se mueven un poco verticalmente.

El orden de las "tarjetas escalables en 3D" se puede cambiar usando la secuencia ( sequenced) de gestos LongPressGesturey DragGesture, que actúa solo en la "tarjeta" superior con indexChat == 0:



Puede hacer clic ( LongPress) en la "tarjeta" superior con un "conjunto de Gráficos", y luego tirar Draghacia abajo ( ) lo suficiente como para mirar la siguiente carta, y si continúa arrastrándola hacia abajo, "va" al último lugar ZStacky aparece la siguiente "carta":



Además, para la "carta" superior podemos aplicar TapGesture, lo que actuará junto con gestos LongPressGesturey un DragGesture:



Tap"conjunto de gráficos" un gesto mostrará el modal ChartViewcon el correo ementami gestión RangeViewyCheckMarksView :



Solicitud TabViewpara combinar en una pantalla las 3 variantes de la composición "conjunto de gráficos" ChartView.





Tenemos 3 marcadores con una imagen Imagey texto Text, VStackno se necesita una pila vertical para su presentación conjunta.

Corresponden a nuestros 3 métodos de combinación de "conjuntos de gráficos" ChartViews:

  1. "Mesa desplazable" ListChartViews,
  2. pila horizontal con efecto 3D HStackChartViews,
  3. ZStack superpuso "cartas" OverlayCardsViews.

Todos los elementos de interacción del usuario: moverse a lo largo de la línea de tiempo y cambiar la "escala" con la ayuda mini - map, el indicador y los botones para ocultar los "Gráficos". funciona completamente en los 3 casos.

El código está en Github .

SwiftUI ...


Debe familiarizarse con los tutoriales en video, los libros y los blogs:

Mang To , Permite construir esa aplicación , así como una descripción de algunas aplicaciones SwiftUI ,
un libro gratuito "SwiftUI por ejemplo" y un video www.hackingwithswift.com/quick-start/swiftui
- un libro pagado pero la mitad se puede descargar de forma gratuita www.bigmountainstudio.com/swiftui-views-book
- curso de 100 días con SwiftUI www.hackingwithswift.com/articles/201/start-the-100-days-of-swiftui , que comienza ahora y finalizará el 31 de diciembre de 2019,
- cosas impresionantes en SwiftUI se hacen en swiftui-lab.com
- Majid blog ,
- en pointFree.cowww.pointfree.co el "maratón" de publicaciones sobre el uso de Reductores en SwiftUI (súper interesante)
es una maravillosa aplicación MovieSwiftUI que ha tomado algunas ideas.

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


All Articles