SwiftUI для прошлого конкурсного задания Telegram Charts (март 2019 года): все просто

pt1z21temtcp4omtiecaoojqrqa.png

Сразу начну с замечания о том, что приложение, о котором пойдет речь в этой статье, требует Xcode 11 и MacOS Catalina, если вы хотите использовать Live Previews, и Mojave, если будете пользоваться симулятором. Код приложения находится на Github.

В этом году на WWDC 2019, Apple анонсировала SwiftUI, новый декларативный способ построения пользовательского интерфейса (UI) на всех устройствах Apple. Это практически полное отступление от привычного нам UIKit, и я — как и многие другие разработчики — очень хотела посмотреть этот новый инструмент в действии.

В этой статье представлен опыт решение с помощью SwiftUI некоторой задачи, код которой в рамках UIKit несопоставимо более сложный и его не удается на мой взгляд представить в читабельном виде.
Задача связанна с прошлым конкурсом Telegram для Android, iOS and JS разработчиков, который проходил в период 10 — 24 марта 2019 года. В этом конкурсе была предложена простая задача графического отображения интенсивности использования некоторого ресурса в интернете в зависимости от времени на основе JSON данных. Как iOS разработчик вы должны использовать язык Swift для представления на конкурс кода, написанного «с нуля» без использования каких-либо посторонних специализированных библиотек для построения графиков.

Эта задача требовала навыков работы с графическими и анимационными возможностями iOS:  Core Graphics, Core Animation, Metal, OpenGL ES. Некоторые из этих инструментов являются низкоуровневыми, не объектно-ориентированными средствами программирования. По существу, в iOS не было приемлемых шаблонов для решения подобных, казалось бы, легких на первый взгляд графических задач. Поэтому каждый конкурсант изобретал свой собственный аниматор (Render) на основе Metal, CALayers, OpenGL, CADisplayLink. Это порождало тонны кода, из которого ничего не удавалось заимствовать и развивать, так как это чисто «авторские» работы, которые реально могут развивать только авторы. Однако так быть не должно.

И вот в начале июня на WWDC 2019 появляется SwifUI — новый framework, разработанный Apple, написанный на Swift и предназначенный для декларативного описания пользовательского интерфейса (UI) в коде. Вы определяете, какие subviews показываются в вашем View, какие данные заставляют эти subviews изменяться, какие модификаторы к ним нужно применить, чтобы заставить их позиционироваться в нужном месте, иметь нужный размер и стиль. Не менее важным элементом SwiftUI является управление потоком изменяемых пользователем данных, которые в свою очередь обновляют UI.

В этой статье я хочу показать, как просто и быстро решается та самая задача конкурса Telegram на SwiftUI. Кроме того это очень увлекательный процесс.


Конкурсное приложение должно показывать одновременно на экране 5 «наборов Графиков», используя предоставленные Telegram данные. Для одного «набора Графиков» UI выглядит следующим образом:

6jmeai2ecva9nvxoc8rbdki9qbu.png

В верхней части расположена «зона Графиков» с общим масштабом по обычной оси Y с отметками и горизонтальными линиями сетки. Чуть ниже расположена «бегущая строка» с временными отметками по оси X в виде дат.

Еще ниже располагается так называемый «mini map» (как в Xcode 11), то есть прозрачное «окошко», определяющее ту часть временного отрезка наших «Графиков», которая более подробно представлена в верхней «зоне Графиков». Этот «mini map» можно не только перемещать вдоль оси X, но и менять его ширину, что сказывается на временном масштабе в «зоне Графиков».

С помощью checkboxs, окрашенных в цвета «Графиков» и снабженных их названиями, можно отказаться от показа соответствующего этому цвету «Графика» в «зоне Графиков».

Таких «наборов Графиков» много, в нашем тестовом примере их, например, 5, и все они должны располагаться на одном экране. 

В UI, проектируемом с помощью SwiftUI нет необходимости в кнопке переключения между Dark и Light режимами, это уже встроено в SwiftUI. Кроме того, в SwiftUI гораздо больше возможностей комбинирования «наборов Графиков» (то есть множества представленных выше экранов), чем просто прокручиваемая вниз таблица, и мы рассмотрим некоторые из этих очень интересных вариантов.

Но сначала остановимся на отображении одного «набора Графиков», для которого в SwiftUI создадим ChartView:

c9fsjthmivuphsmdbvajccjfg18.png

SwiftUI позволяет создавать и тестировать сложный UI по маленьким кусочкам, а потом очень просто собирать эти кусочки в пазл. Мы так и поступим. Наш  ChartView очень хорошо расщепляется на эти маленькие кусочки:

  • GraphsForChart — это собственно графики, построенные для одного конкретного «набора Графиков». «Графики» показаны для временного диапазона, управляемого пользователем с помощью «mini map» RangeView, который будет представлен ниже.
  • YTickerView — ось Y с отметками и соответствующей горизонтальной сеткой.
  • IndicatorView — горизонтально перемещаемый пользователем индикатор, позволяющий посмотреть значения «Графиков» и времени для соответствующего положения индикатора на временной на оси X.
  • TickerView — «бегущая строка», показывающая временные отметки на оси X в виде дат,
  • RangeView — временное «окошко», настраиваемое пользователем с помощью жестов, для задания временного интервала «Графиков»,
  • CheckMarksView — содержит «кнопки», окрашенные в цвета «Графиков» и позволяющие управлять присутствием «Графика» на ChartView .


С ChartView пользователь может взаимодействовать тремя способами:

1. управлять«mini map» с помощью жеста DragGesture — он может сдвигать временное «окошко» вправо и влево и уменьшать / увеличивать его размер:

icyc4bxgrxyq0wiu1sxuqg5ft10.png

2. перемещать в горизонтальном направлении индикатор, показывающий значения «Графиков» в фиксированный момент времени:

hpkctt_b-gbljvhetcylfxkdtxa.png

3. скрывать / показывать определенные «Графики» с помощью кнопок, окрашенных в цвета «Графиков» и расположенных в самом низу ChartView:

jmdm2xiezy_vznyu8rwpkkmnjou.png

Мы можем комбинировать различные «Наборы Графиков» (их у нас 5 в тестовых данных) разными способами, например, расположив их все одновременно на одном экране с помощью списка List (наподобие прокручиваемой вниз-вверх таблицы):

-yahdowfzpcqeee7ewn0phjkn7y.png

или с помощью ScrollView и горизонтального стека HStack c 3D эффектом:

sz-c1s8eycfzb1krumjgr7oxteg.png

… или в виде ZStack наложенных друг на друга «карт», порядок которых можно менять: верхнюю «карту» с «набором Графиков» можно оттянуть вниз достаточно далеко, чтобы посмотреть на следующую карту, и если продолжать тянуть ее вниз, то она «уходит» на последнее место в ZStack, а вперед «выходит» эта следующая «карта»:

0epzmhcuuqif2_um7uew4r0z_l8.png

В этих сложных UI — «прокручиваемая таблица», горизонтальный стек с 3D эффектом, ZStack наложенных друг на друга «карт» — полноценно работают все средства взаимодействия с пользователем: перемещение по временной шкале и изменение «масштаба» mini - map, индикатор и кнопки скрытия «Графиков».

Далее мы будем подробно рассматривать проектирование этого UI с помощью SwiftUI — от простейших элементов к их более сложным композициям. Но сначала поймем структуру данных, которыми мы располагаем.

Итак, решение нашей задачи разбилось на несколько этапов:

  • Закачать данные из JSON-файла и представить их в удобном «внутреннем» формате
  • Создать UI для одного «набора Графиков»
  • Комбинировать различные «наборы Графиков»


Закачиваем данные


В наше распоряжение Telegram предоставил JSON данные, содержащие несколько «наборов Графиков». Каждый отдельный «набор Графиков» chart содержит несколько «Графиков» (или «Линий») chart.columns. У каждого «Графика» («Линии») есть метка в позиции 0 — "x", "y0", "y1", "y2", "y3", за которой следуют либо значения времени на оси X («x»), либо значения «Графика» («Линии») ("y0", "y1", "y2", "y3") на оси Y :

_jqtex2-hskpvedg3imtrlzl20w.png

Присутствие всех «Линий» в «наборе Графиков» — необязательно. Значения для «столбца» x представляют собой UNIX метки времени в миллисекундах.

Кроме того, каждый отдельный «набор Графиков» chart снабжается цветами chart.colors в формате 6-ти шестнадцатеричных цифр (например,»#AAAAAA») и именами chart.names.

Для построения Модели данных, находящихся в JSON-файле, я воспользовалась прекрасным сервисом quicktype. На этом сайте вы вставляете кусок текста из JSON файла и указываете язык программирования (Swift), имя структуры (Chart), которая сформируется после «парсинга» этих JSON данных и всё.

В центральной части экрана формируется код, который мы скопируем в наше приложение в отдельный файл с именем Chart.swift. Именно там мы будем размещать Модель данных JSON формата. Воспользовавшись заимствованным из демонстрационных примерах SwiftUI Generic загрузчиком load данных из JSON файла в Модель, я получила массив columns: [ChartElement], представляющий собой совокупность «наборов Графиков» в заданном Telegram формате.

Cтруктура данных ChartElement, содержащая массивы разнотипных элементов,  не очень подходит для интенсивной интерактивной работы с графиками, кроме того метки времени представлены в UNIX формате в миллисекундах (например,  1542412800000, 1542499200000, 1542585600000, 1542672000000), а цвета — в формате 6-ти шестнадцатеричных цифр (например, "#AAAAAA").

Поэтому внутри нашего приложения мы будем пользоваться теми же данными, но в другом «внутреннем» и довольно простом формате [LinesSet]. Массив [LinesSet] представляет собой совокупность «наборов Графиков» LinesSet, каждый из которых содержит временные метки xTime в формате "Feb 12, 2019" (ось X) и несколько «Графиков» lines (ось Y):

rsb_updm-bojah79bsz23bdu6k0.png

Данные для каждого «Графика»(«Линии») Line представлены

  • массивом целых чисел points: [Int],
  • именем «Графика» title: String,  
  • типом «Графика» type: String?,   
  • цветом color : UIColor в свойственном для Swift формате UIColor,
  • количеством точек countY: Int.


Кроме того, любой «График» может быть скрыт или показан в зависимости от значения isHidden: Bool. Параметры lowerBound и upperBound регулировки временного диапазона принимают значения от 0 до 1 и показывают для заданного «набора Графиков» не только размер временного «окошка» «mini map» (upperBound —  lowerBound), но и его местоположение на временной оси X:

hps5_vebuvf83hxiethp57hiho0.png

Структуры JSON данных [ChartElement] и структуры данных «внутреннего» представления LinesSet и Line находятся в файле Chart.swift. Код для загрузки JSON данных и преобразования их во внутреннюю структуру находится  в файле Data.swift. Подробно об этих преобразованиях можно узнать здесь.

В результате мы получили данные о «наборах Графиков» во внутреннем формате в виде массива chartsData.

7iy0bk7mifanhlnph6euwk7lg8o.png

Это и есть наша Модель данных, но для работы в SwiftUI необходимо сделать так, чтобы любые изменения, выполненные пользователем в массиве chartsData (изменение временного «окошка», скрытие / показ «Графиков») приводили к автоматическим обновлениям наших Views.

Мы создадим @EnvironmentObject. Это позволит нам использовать Модель данных везде, где это необходимо, и кроме этого, автоматически обновлять наши Views, если данные будут меняться. Это что-то типа Singleton или глобальных данных.

@EnvironmentObject требует от нас создания некоторого класса final class UserData, который находится в файле UserData.swift, запоминает данные chartsData и реализует протокол ObservableObject:

hr3ingagv3dfwfjlgasjtsiy6ye.png

Наличие @Published «обертки» позволит разместить «новости» о том, что данные свойства charts класса UserData изменились, так что любые Views, «подписанные на эти новости» в SwiftUI, смогут автоматически выбрать новые данные и обновиться. 

Напомним, что в свойстве charts могут меняться значения isHidden для любого «Графика» (они позволяют скрывать или показывать эти «Графики»), а также нижняя lowerBound и верхняя upperBound границы временного интервала для каждого отдельного «набора Графиков».

Свойство charts класса UserData мы хотим использовать повсюду в нашем приложении и нам не придется синхронизировать их с UI вручную благодаря @EnvironmentObject.

Для этого при старте приложения мы должны создать экземпляр класса UserData (), чтобы впоследствие иметь к нему доступ где угодно в нашем приложении. Мы сделаем это в файле SceneDelegate.swift внутри метода scene (_ : , willConnectTo: , options: ). Именно там создается и запускается наш ContentView, и именно здесь мы должны передавать ContentView любые созданные нами @EnvironmentObject так, чтобы SwiftUI мог сделать их доступными для любого другого View:

jv20uihyiuv6xnkm13zt7o0him8.png

Теперь, в любом View для доступа к @Published данным класса UserData нам нужно создать переменную var, используя @EnvironmentObject обертку. Например, при настройке временного диапазона в RangeView мы создаем переменную var userData, имеющую ТИП UserData:

sxlrvyrfvqphxtkqxhblnsyf5as.png

Итак, как только мы внедрили некоторый объект @EnvironmentObject в «среду» приложения, мы можем немедленно начать его использовать либо на самом верхнем уровне, либо 10-ю уровнями ниже — это не имеет значения. Но что более важно, всякий раз, когда какое-то View изменит «среду», все Views, имеющие этот @EnvironmentObject, автоматически обновятся, обеспечивая тем самым синхронизацию с данными.

Перейдем к проектированию пользовательского интерфейса (UI).

Пользовательский Интерфейс (UI) для одного «набора Графиков»


SwiftUI предлагает композиционную технологию создания UI из множества небольших Views, а мы уже видели, что наше приложение очень хорошо ложится на эту технологию, так как расщепляется на маленькие кусочки: «набор Графиков» ChartView, «Графики» GraphsForChart, отметки на оси Y — YTickerView, управляемый пользователем индикатор значений «Графиков» IndicatorView, «бегущую» строку TickerView с временными отметками на оси X , управляемое пользователем «временное окно» RangeView, отметки о скрытии / показе «Графиков» CheckMarksView. Все эти Views мы можем не только создавать независимо друг от друга, но тут же и тестировать в Xcode 11 с помощью Previews (предварительных «живых» просмотров) на тестовых данных. Вы удивитесь насколько прост код для их создания из других более элементарных Views .

GraphView — «График» («Линия»)


Первое View, с которого мы начнем, — это собственно сам «График» (или «Линия»). Мы назовем его GraphView:

elqvglloxidiuisuqfa6ue_ppgm.png

Создание GraphView, как обычно, начинается с создания нового файла в Xcode 11 с помощью меню FileNewFile:

e5cj36vuvyrgldcpsvoe-h49t8y.png

Затем мы выбираем нужный ТИП файла — это SwiftUI файл:

ojqvuznrwfj2eak_rdxchb1zebo.png

… даем название «GraphView» нашему View и указываем его местоположение:

ulk_hhp_irtcrjs3kau_gl1jtbo.png

Кликаем на кнопке "Create" и получаем стандартное View с текстом Text ( "Hello  World!") в середине экрана:

lt-gdm28u35e_j0fcqntlfkzzk4.png

Наша задача — заменить текст Text ("Hello World!") на «График», но сначала давайте посмотрим, какими исходными данными для создания «Графика» мы располагаем:

  • у нас есть значения line.points «Графика» line: Line,
  • временной диапазон rangeTime,  представляющий собой диапазон индексов Range временных отметок xTime на ОСИ X,
  • диапазон значений rangeY: Range «Графика» для ОСИ Y,
  • толщина линии обводки «Графика» lineWidth.


Добавляем эти свойства в структуру GraphView:

u1oxyk-mytozrngercig59livp4.png

Если мы хотим использовать для нашего «Графика» Previews (предварительные просмотры), которые возможны только для MacOS  Catalyna, то мы должны инициировать GraphView с диапазон индексов rangeTime и данными line самого «Графика»:

kepmffuuv1oqjczavsmk4ylehsm.png

У нас уже есть тестовые данные chartsData, которые мы получили из JSON файла chart.json, и мы их использовали для Previews.

В нашем случае это будет первый «набор Графиков» chartsData[0] и первый «График» в этом наборе chartsData[0].lines[0], который мы предоставим GraphView в качестве параметра line.

В качестве временного интервала rangeTime мы будем использовать полный диапазон индексов 0..<(chartsData[0].xTime.count - 1).
Параметры rangeY и lineWidth можно задавать извне, а можно и не задавать, так как у них уже есть начальные значения: у rangeY — это nil, а у  lineWidth — 1.

Мы намеренно сделали ТИП свойства rangeY  Optional ТИПОМ, так как в случае, если rangeY не задается извне и rangeY = nil, то мы вычисляем минимальное minY и максимальное maxY значения «Графика» непосредственно из данных line.points:

hm8stdxllwz2cjz-mx1mribdmz0.png

Этот код компилируется, но мы по-прежнему имеем на экране  стандартное View с текстом Text ("Hello World!") в середине экрана:

a7gbcbzmh8o2vqwvxjaymj_-efq.png

Потому что в body мы должны заменить текст Text ("Hello World!") на Path, который по точкам line.points с помощью команды addLines(_:) (почти как в Core Graphics) будет строить наш «График:

d8kksod7xystc05pnet590ffnqi.png
u4g_oozbtounjhimpueunibqimw.png

Мы обведем stroke (...) наш Path линией, толщина которой равняется lineWidth, при этом цвет линии обводки будет соответствовать цвету «по умолчанию» (то есть «черному»):

gohbhaazgmzagwgb39hbwxv1r7m.png

Мы можем заменить черный цвет для линии обводки на цвет, заданный в нашем конкретном «Графике» line.color:

jprue-t_4pthzn2jov-uqunbvim.png

Для того, чтобы наш «График» мог размещаться в прямоугольниках любых размеров, мы используем контейнер GeometryReader. В документации Apple GeometryReader — это «контейнер» View, который определяет свое содержимое как функцию от собственных размера size и координатного пространства. По существу, GeometryReader — это еще одно View! Потому что почти ВСЁ в SwiftUI является View!  GeometryReader позволит ВАМ в отличие от других Views получить доступ к некоторой дополнительной полезной информации, которой можно воспользоваться при проектировании вашего пользовательского View.

Мы используем контейнер GeometryReader и Path для создания адаптируемого к любым размерам GraphView. И если мы посмотрим внимательно на наш код, то увидим в замыкании для GeometryReader переменную с именем geometry:

_s0d3ycqsymzwtqhqril4ycawru.png

Эта переменная имеет ТИП GeometryProxy, который в свою очередь является структурой struct со множеством «сюрпризов»:

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


Из определения GeometryProxy мы видим, что там присутствуют две вычисляемые переменные var size и var safeAreaInsets, одна функция frame( in:) и subscript getter. Нам понадобилась только переменная size для определения ширины geometry.size.width и высоты geometry.size.height области рисования «Графика».

Кроме того, мы даем возможность нашему «Графику» анимировать с помощью модификатора animation (.linear(duration: 0.6)).

fikc4uzehzfzkwvqza6zm_curwy.png

GraphView_Previews позволяет нам очень просто тестировать любые «Графики» из любого «набора». Ниже представлен «График» из «набора Графиков» с индексом 4:  chartsData[4] и индексом 0 «Графика» в этом наборе:  chartsData[4].lines[0] :

gknv74w88p7hv46koyptkkkqn-k.png

Мы задали высоту height «Графика» равной 400 с помощью frame (height: 400), ширина осталась равной ширине экрана. Если бы мы не использовали frame (height: 400), то «График» занял бы весь экран. Мы не задали диапазон значений rangeY и GraphView использовал значение nil, которое задано по умолчанию, в этом случае «График» берет свои минимальное и максимальное значения на временном интервале rangeTime:

ganmc6t8wiap8exed4yxqwkgyqo.png

Хотя мы применили для нашего Path модификатор animation (.linear(duration: 0.6)), никакой анимации происходить не будет, например, при изменении диапазона rangeY значений «Графика». «График» будет просто «прыгать» от одного значения диапазона rangeY к другому без всякой анимации. 

Причина простая: мы научили SwiftUI тому, как нарисовать «График» для конкретного диапазона rangeY , но мы не научили SwiftUI тому, как воспроизводить «График» многократно с промежуточными  значениями диапазона rangeY между начальным и конечным, а за это в SwiftUI отвечает протокол Animatable

К счастью, если ваш View —  «фигура», то есть View, которое реализует протокол Shape, то для него уже реализован протокол Animatable. Это означает, что существует вычисляемое свойство animatableData, с помощью которого мы можем управлять процессом анимации,  но по умолчанию оно установлено в EmptyAnimatableData, то есть никакой анимации не происходит.

 Для того, чтобы решить проблему с анимацией, мы сначала должны превратить наш «График» GraphView в Shape. Это очень просто, нам нужно только реализовать функцию func path (in rect:CGRect) -> Path, которая у нас, по существу, уже есть и указать с помощью вычисляемого свойства animatableData, какие данные мы хотим анимировать:

iexmgtfxveueipqyopubpbb_3uc.png

Отметим, что тема управления анимацией является продвинутой темой в SwiftUI и вы можете более подробно с ней познакомиться в статье «Advanced SwiftUI Animations — Part 1: Paths».

Полученную «фигуру» Graph мы можем использовать в значительно более простом GraphViewNew для «Графика» с анимацией:  

yqqkcah_txfmxn3zsaiixwbmjak.png

Вы видите, что нам не понадобился GeometryReader для нашего нового «Графика» GraphViewNew, так как благодаря протоколу Shape наша «фигура» Graph сможет адаптироваться к любому размеру родительского View.

Естественно в Previews мы получили тот же самый результат, что и в случае с GraphView:

bodntbqfjjkcnwqyzvhzkljand0.png

В последующих комбинациях мы будем использовать GraphViewNew для отображения значений одного «Графика».

GraphsForChart — совокупность «Графиков» («Линий»)


Задача этого View — отображать ВСЕ «Графики» («Линии») из «набора Графиков» chart в заданном временном диапазоне rangeTime с общей осью Y, при этом ширина «Линий» равна lineWidth:

ypnjeuqfnjxvrzwe-mfe8ui7slo.png

Также как и для GraphView и GraphViewNew, мы создадим для GraphsForChart новый файл GraphsForChart.swift и определяем исходные данные для «набора Графиков»:

  • сам «набор Графиков» chart: LineSet (значения на ОСИ Y),
  • диапазон rangeTime: Range  (ОСЬ X) индексов временных отметок «Графиков»,
  • толщина линии обводки «Графиков» lineWidth


Диапазон значений rangeY: Range для «набора Графиков» (ОСЬ Y) вычисляется как объединение диапазонов отдельных не cкрытых (isHidden = false) «Графиков», входящих в данный «набор»:

xbjdmjbwiyguhmplj1znuy794fq.png

Для этого мы используем функцию rangeOfRanges:

w-uhhrzrrdp8wjznihwiduyatni.png

Все НЕ скрытые «Графики» ( isHidden = false) мы показываем в ZStack с помощью конструкции ForEach, наделяя при этом каждый «График» возможностью появления на экране и ухода с экрана «с помощью модификатора «перемещения» transition(.move(edge: .top)):

elpen5p3hgqi9n3izkbwpssfeo4.png

Благодаря этому модификатору процесс скрытия и возвращения «Графика» в ChartView будет проходить на экране с анимацией и даст понять пользователю, почему изменился масштаб по оси Y.

Использование drawingGroup() означает использование Metal для рисования графических фигур. На наших тестовых данных и на симуляторе вы не почувствуете разницы в скорости рисования с Metal и без Metal, но если вы воспроизводите множество достаточно громоздких графиков на любом iPhone, то вы заметите эту разницу. Для более подробного ознакомления, когда следует использовать drawingGroup(), можно посмотреть статью «Advanced SwiftUI Animations — Part 1: Paths» или посмотреть видео сессии 237 WWDC 2019 (Building Custom Views with SwiftUI).

Как и в случае с GraphViewNew при тестировании GraphsForChart с помощью предварительных просмотров Previews мы можем установить любой «набор Графиков», например, с индексом 0:

3f0sv_wpwemxkrdtsjdn2utz_ca.png

IndicatorView — горизонтально перемещаемый индикатор «Графика».


Этот индикатор позволяет получить точные значения «Графиков» и времени для соответствующей точки на временной на оси X:

vixtsqqwrjuu3dj55vzmhxyibhy.png

Индикатор создается для определенного «набора Графиков» chart и состоит из скользящей вдоль оси X вертикальной ЛИНИИ с ОТМЕТКАМИ на ней в виде «кружочков» в месте значений «Графиков». К верхней части этой вертикальной линии прикреплен небольшой «ПЛАКАТ», содержащий численные значения «Графиков» и времени.

nu0q7nmjrtoqgcaaaasp2b3ukks.png

Скольжение индикатора производит пользователь с помощью жеста DragGesture:

v44wzgzsujx-w7f9iymlfuijlia.png

Мы используем так называемое «инкрементное» выполнение жеста. Вместо непрерывного расстояния от стартовой точки value.translation.width, мы будем в обработчике onChanged постоянно получать расстояние от того места, где были в прошлый раз, когда выполняли жест:  value.translation.width - self.prevTranslation. Это обеспечит нам плавное перемещение индикатора.

Для тестирования индикатора IndicatorView с помощью Previews для заданного «набора Графиков» chart мы можем привлечь уже готовое View построения «Графиков» GraphsForChart:

cecboyk2-z74i_zdenfvkl36kya.png

Мы можем задать любой, но согласованный друг с другом, диапазон времени rangeTime как для индикатора IndicatorView, так и для «Графиков» GraphsForChart. Это позволит нам убедиться, что «кружочки», обозначающие значения «Графиков», находятся на правильных местах.

TickerView — ОСЬ X с отметками.


Пока наши «Графики» обезличены в том смысле, что у них НЕТ ОСЕЙ X и Y с соответствующими масштабами и отметками. Давайте нарисуем ОСЬ X с временными отметками TickerMarkView на ней. Сами отметки TickerMarkView представляют собой очень простой View с вертикальным стеком VStack, в котором размещены Path и Text:

qj5fmst7za8mibfd16lv43fq_wa.png

Совокупность отметок на временной оси для определенного «набора Графиков» chart : LineSet формируется в TickerView в соответствие с выбранным пользователем временным диапазоном rangeTime и приблизительным количеством отметок estimatedMarksNumber, которые должны оказаться в поле зрения пользователя:

6ncqpdiagf-023ftarlzz_qaqgo.png

Для расположения «бегущих» отметок времени используем ScrollView и горизонтальный стек HStack, который будет смещаться по мере изменения временного диапазона rangeTime.

В TickerView мы формируем шаг step, с которым появляются отметки времени TimeMarkView, основываясь на заданном временном диапазоне rangeTime и ширине экрана widthRange

dv0aum4_9qfirikrlgzpquc4tio.png

…, а затем выбираем отметки времени c шагом step из массива chart.xTime с помощью индексов indexes.

Собственно ОСЬ X — горизонтальную прямую — мы наложим overlay …

_z_zbh7qxng8xsqeor_mje-4bb8.png

… на горизонтальный стек HStack, с отметками времени TimeMarkView, который мы продвигаем с помощью offset:

lxhahaxobvg1e0novqhdqcv6ju8.png

Кроме этого, мы можем задавать цвета самой ОСИ X — colorXAxis, и отметок — colorXMark:

fpe5lcp-_a4e_okpgb3hmbpopto.png

YTickerView — ОСЬ Y с отметками и сеткой.


Этот View рисует ОСЬ Y с цифровыми отметками YMarkView. Сами отметки YMarkView представляют собой очень простой View с вертикальным стеком VStack, в котором размещены Path (горизонтальная линия) и Text с числом:

sl1lu-qb3vu0prdm20x1mml8uxs.png

Совокупность отметок на ОСИ Y для определенного «набора Графиков» chart формируется в YTickerView. Диапазон значений rangeY вычисляется как объединение диапазонов значений всех «Графиков», входящих в данный «набор Графиков» с помощью функции rangeOfRanges. Приблизительное количество отметок на ОСИ Y задается параметром estimatedMarksNumber:

2i0ts6fkosuamlcuekwltqp8ars.png

В YTickerView мы отслеживаем изменение диапазона значений «Графиков» rangeY. Собственно ОСЬ Y — вертикальную прямую — мы накладываем overlay на наши отметки…

fpuxvd0yeuhcliigesjup79ep0g.png

Кроме этого, мы можем задавать цвета самой ОСИ Y — colorYAxis, и отметок — colorYMark:

p3j9fx2xe0y0rowdofhru1drlr8.png

RangeView — настройка временного диапазона с помощью «mini-map».


Самой подвижной частью

© Habrahabr.ru