SwiftUI для прошлого конкурсного задания Telegram Charts (март 2019 года): все просто
Сразу начну с замечания о том, что приложение, о котором пойдет речь в этой статье, требует 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
выглядит следующим образом:
В верхней части расположена «зона Графиков» с общим масштабом по обычной оси Y с отметками и горизонтальными линиями сетки. Чуть ниже расположена «бегущая строка» с временными отметками по оси X в виде дат.
Еще ниже располагается так называемый «mini map» (как в Xcode 11
), то есть прозрачное «окошко», определяющее ту часть временного отрезка наших «Графиков», которая более подробно представлена в верхней «зоне Графиков». Этот «mini map» можно не только перемещать вдоль оси X
, но и менять его ширину, что сказывается на временном масштабе в «зоне Графиков».
С помощью checkboxs
, окрашенных в цвета «Графиков» и снабженных их названиями, можно отказаться от показа соответствующего этому цвету «Графика» в «зоне Графиков».
Таких «наборов Графиков» много, в нашем тестовом примере их, например, 5, и все они должны располагаться на одном экране.
В UI
, проектируемом с помощью SwiftUI
нет необходимости в кнопке переключения между Dark
и Light
режимами, это уже встроено в SwiftUI
. Кроме того, в SwiftUI
гораздо больше возможностей комбинирования «наборов Графиков» (то есть множества представленных выше экранов), чем просто прокручиваемая вниз таблица, и мы рассмотрим некоторые из этих очень интересных вариантов.
Но сначала остановимся на отображении одного «набора Графиков», для которого в SwiftUI
создадим ChartView
:
SwiftUI
позволяет создавать и тестировать сложный UI
по маленьким кусочкам, а потом очень просто собирать эти кусочки в пазл. Мы так и поступим. Наш ChartView
очень хорошо расщепляется на эти маленькие кусочки:
GraphsForChart
— это собственно графики, построенные для одного конкретного «набора Графиков». «Графики» показаны для временного диапазона, управляемого пользователем с помощью «mini map»RangeView
, который будет представлен ниже.YTickerView
— осьY
с отметками и соответствующей горизонтальной сеткой.IndicatorView
— горизонтально перемещаемый пользователем индикатор, позволяющий посмотреть значения «Графиков» и времени для соответствующего положения индикатора на временной на осиX
.TickerView
— «бегущая строка», показывающая временные отметки на осиX
в виде дат,RangeView
— временное «окошко», настраиваемое пользователем с помощью жестов, для задания временного интервала «Графиков»,CheckMarksView
— содержит «кнопки», окрашенные в цвета «Графиков» и позволяющие управлять присутствием «Графика» наChartView
.
С ChartView
пользователь может взаимодействовать тремя способами:
1. управлять«mini map» с помощью жеста DragGesture
— он может сдвигать временное «окошко» вправо и влево и уменьшать / увеличивать его размер:
2. перемещать в горизонтальном направлении индикатор, показывающий значения «Графиков» в фиксированный момент времени:
3. скрывать / показывать определенные «Графики» с помощью кнопок, окрашенных в цвета «Графиков» и расположенных в самом низу ChartView
:
Мы можем комбинировать различные «Наборы Графиков» (их у нас 5 в тестовых данных) разными способами, например, расположив их все одновременно на одном экране с помощью списка List
(наподобие прокручиваемой вниз-вверх таблицы):
или с помощью ScrollView
и горизонтального стека HStack
c 3D эффектом:
… или в виде ZStack
наложенных друг на друга «карт», порядок которых можно менять: верхнюю «карту» с «набором Графиков» можно оттянуть вниз достаточно далеко, чтобы посмотреть на следующую карту, и если продолжать тянуть ее вниз, то она «уходит» на последнее место в ZStack
, а вперед «выходит» эта следующая «карта»:
В этих сложных 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
:
Присутствие всех «Линий» в «наборе Графиков» — необязательно. Значения для «столбца» 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
):
Данные для каждого «Графика»(«Линии») Line
представлены
- массивом целых чисел
points: [Int]
, - именем «Графика»
title: String
, - типом «Графика»
type: String?
, - цветом
color : UIColor
в свойственном дляSwift
форматеUIColor
, - количеством точек
countY: Int
.
Кроме того, любой «График» может быть скрыт или показан в зависимости от значения isHidden: Bool
. Параметры lowerBound
и upperBound
регулировки временного диапазона принимают значения от 0
до 1
и показывают для заданного «набора Графиков» не только размер временного «окошка» «mini map» (upperBound
— lowerBound
), но и его местоположение на временной оси X
:
Структуры JSON
данных [ChartElement]
и структуры данных «внутреннего» представления LinesSet
и Line
находятся в файле Chart.swift. Код для загрузки JSON
данных и преобразования их во внутреннюю структуру находится в файле Data.swift. Подробно об этих преобразованиях можно узнать здесь.
В результате мы получили данные о «наборах Графиков» во внутреннем формате в виде массива chartsData
.
Это и есть наша Модель
данных, но для работы в SwiftUI
необходимо сделать так, чтобы любые изменения, выполненные пользователем в массиве chartsData
(изменение временного «окошка», скрытие / показ «Графиков») приводили к автоматическим обновлениям наших Views
.
Мы создадим @EnvironmentObject
. Это позволит нам использовать Модель
данных везде, где это необходимо, и кроме этого, автоматически обновлять наши Views
, если данные будут меняться. Это что-то типа Singleton
или глобальных данных.
@EnvironmentObject
требует от нас создания некоторого класса final class UserData
, который находится в файле UserData.swift, запоминает данные chartsData
и реализует протокол ObservableObject
:
Наличие @Published
«обертки» позволит разместить «новости» о том, что данные свойства charts
класса UserData
изменились, так что любые Views
, «подписанные на эти новости» в SwiftUI
, смогут автоматически выбрать новые данные и обновиться.
Напомним, что в свойстве charts
могут меняться значения isHidden
для любого «Графика» (они позволяют скрывать или показывать эти «Графики»), а также нижняя lowerBound
и верхняя upperBound
границы временного интервала для каждого отдельного «набора Графиков».
Свойство charts
класса UserData
мы хотим использовать повсюду в нашем приложении и нам не придется синхронизировать их с UI
вручную благодаря @EnvironmentObject
.
Для этого при старте приложения мы должны создать экземпляр класса UserData ()
, чтобы впоследствие иметь к нему доступ где угодно в нашем приложении. Мы сделаем это в файле SceneDelegate.swift
внутри метода scene (_ : , willConnectTo: , options: )
. Именно там создается и запускается наш ContentView
, и именно здесь мы должны передавать ContentView
любые созданные нами @EnvironmentObject
так, чтобы SwiftUI
мог сделать их доступными для любого другого View
:
Теперь, в любом View
для доступа к @Published
данным класса UserData
нам нужно создать переменную var
, используя @EnvironmentObject
обертку. Например, при настройке временного диапазона в RangeView
мы создаем переменную var userData
, имеющую ТИП UserData
:
Итак, как только мы внедрили некоторый объект @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
:
Создание GraphView
, как обычно, начинается с создания нового файла в Xcode 11
с помощью меню File
→ New
→ File
:
Затем мы выбираем нужный ТИП файла — это SwiftUI
файл:
… даем название «GraphView» нашему View
и указываем его местоположение:
Кликаем на кнопке "Create"
и получаем стандартное View
с текстом Text ( "Hello World!")
в середине экрана:
Наша задача — заменить текст Text ("Hello World!")
на «График», но сначала давайте посмотрим, какими исходными данными для создания «Графика» мы располагаем:
- у нас есть значения
line.points
«Графика»line: Line
, - временной диапазон
rangeTime
, представляющий собой диапазон индексовRange
временных отметокxTime
на ОСИ X, - диапазон значений
rangeY: Range
«Графика» для ОСИ Y, - толщина линии обводки «Графика»
lineWidth
.
Добавляем эти свойства в структуру GraphView
:
Если мы хотим использовать для нашего «Графика» Previews
(предварительные просмотры), которые возможны только для MacOS Catalyna
, то мы должны инициировать GraphView
с диапазон индексов rangeTime
и данными line
самого «Графика»:
У нас уже есть тестовые данные 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
:
Этот код компилируется, но мы по-прежнему имеем на экране стандартное View
с текстом Text ("Hello World!")
в середине экрана:
Потому что в body
мы должны заменить текст Text ("Hello World!")
на Path
, который по точкам line.points
с помощью команды addLines(_:)
(почти как в Core Graphics
) будет строить наш «График:
Мы обведем stroke (...)
наш Path
линией, толщина которой равняется lineWidth
, при этом цвет линии обводки будет соответствовать цвету «по умолчанию» (то есть «черному»):
Мы можем заменить черный цвет для линии обводки на цвет, заданный в нашем конкретном «Графике» line.color
:
Для того, чтобы наш «График» мог размещаться в прямоугольниках любых размеров, мы используем контейнер GeometryReader
. В документации Apple
GeometryReader
— это «контейнер» View
, который определяет свое содержимое как функцию от собственных размера size
и координатного пространства. По существу, GeometryReader
— это еще одно View
! Потому что почти ВСЁ в SwiftUI
является View
! GeometryReader
позволит ВАМ в отличие от других Views
получить доступ к некоторой дополнительной полезной информации, которой можно воспользоваться при проектировании вашего пользовательского View
.
Мы используем контейнер GeometryReader
и Path
для создания адаптируемого к любым размерам GraphView
. И если мы посмотрим внимательно на наш код, то увидим в замыкании для GeometryReader
переменную с именем geometry
:
Эта переменная имеет ТИП 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))
.
GraphView_Previews
позволяет нам очень просто тестировать любые «Графики» из любого «набора». Ниже представлен «График» из «набора Графиков» с индексом 4: chartsData[4]
и индексом 0 «Графика» в этом наборе: chartsData[4].lines[0]
:
Мы задали высоту height
«Графика» равной 400 с помощью frame (height: 400)
, ширина осталась равной ширине экрана. Если бы мы не использовали frame (height: 400)
, то «График» занял бы весь экран. Мы не задали диапазон значений rangeY
и GraphView
использовал значение nil
, которое задано по умолчанию, в этом случае «График» берет свои минимальное и максимальное значения на временном интервале rangeTime
:
Хотя мы применили для нашего 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
, какие данные мы хотим анимировать:
Отметим, что тема управления анимацией является продвинутой темой в SwiftUI
и вы можете более подробно с ней познакомиться в статье «Advanced SwiftUI Animations — Part 1: Paths».
Полученную «фигуру» Graph
мы можем использовать в значительно более простом GraphViewNew
для «Графика» с анимацией:
Вы видите, что нам не понадобился GeometryReader
для нашего нового «Графика» GraphViewNew
, так как благодаря протоколу Shape
наша «фигура» Graph
сможет адаптироваться к любому размеру родительского View
.
Естественно в Previews
мы получили тот же самый результат, что и в случае с GraphView
:
В последующих комбинациях мы будем использовать GraphViewNew
для отображения значений одного «Графика».
GraphsForChart
— совокупность «Графиков» («Линий»)
Задача этого View
— отображать ВСЕ «Графики» («Линии») из «набора Графиков» chart
в заданном временном диапазоне rangeTime
с общей осью Y
, при этом ширина «Линий» равна lineWidth
:
Также как и для GraphView
и GraphViewNew
, мы создадим для GraphsForChart
новый файл GraphsForChart.swift
и определяем исходные данные для «набора Графиков»:
- сам «набор Графиков»
chart: LineSet
(значения наОСИ Y
), - диапазон
rangeTime: Range
(ОСЬ X
) индексов временных отметок «Графиков», - толщина линии обводки «Графиков»
lineWidth
Диапазон значений rangeY: Range
для «набора Графиков» (ОСЬ Y
) вычисляется как объединение диапазонов отдельных не cкрытых (isHidden = false
) «Графиков», входящих в данный «набор»:
Для этого мы используем функцию rangeOfRanges
:
Все НЕ скрытые «Графики» ( isHidden = false
) мы показываем в ZStack
с помощью конструкции ForEach
, наделяя при этом каждый «График» возможностью появления на экране и ухода с экрана «с помощью модификатора «перемещения» transition(.move(edge: .top))
:
Благодаря этому модификатору процесс скрытия и возвращения «Графика» в 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
:
IndicatorView
— горизонтально перемещаемый индикатор «Графика».
Этот индикатор позволяет получить точные значения «Графиков» и времени для соответствующей точки на временной на оси X
:
Индикатор создается для определенного «набора Графиков» chart
и состоит из скользящей вдоль оси X
вертикальной ЛИНИИ с ОТМЕТКАМИ на ней в виде «кружочков» в месте значений «Графиков». К верхней части этой вертикальной линии прикреплен небольшой «ПЛАКАТ», содержащий численные значения «Графиков» и времени.
Скольжение индикатора производит пользователь с помощью жеста DragGesture
:
Мы используем так называемое «инкрементное» выполнение жеста. Вместо непрерывного расстояния от стартовой точки value.translation.width
, мы будем в обработчике onChanged
постоянно получать расстояние от того места, где были в прошлый раз, когда выполняли жест: value.translation.width - self.prevTranslation
. Это обеспечит нам плавное перемещение индикатора.
Для тестирования индикатора IndicatorView
с помощью Previews
для заданного «набора Графиков» chart
мы можем привлечь уже готовое View
построения «Графиков» GraphsForChart
:
Мы можем задать любой, но согласованный друг с другом, диапазон времени rangeTime
как для индикатора IndicatorView
, так и для «Графиков» GraphsForChart
. Это позволит нам убедиться, что «кружочки», обозначающие значения «Графиков», находятся на правильных местах.
TickerView
— ОСЬ X
с отметками.
Пока наши «Графики» обезличены в том смысле, что у них НЕТ ОСЕЙ X и Y
с соответствующими масштабами и отметками. Давайте нарисуем ОСЬ X
с временными отметками TickerMarkView
на ней. Сами отметки TickerMarkView
представляют собой очень простой View
с вертикальным стеком VStack
, в котором размещены Path
и Text
:
Совокупность отметок на временной оси для определенного «набора Графиков» chart : LineSet
формируется в TickerView
в соответствие с выбранным пользователем временным диапазоном rangeTime
и приблизительным количеством отметок estimatedMarksNumber
, которые должны оказаться в поле зрения пользователя:
Для расположения «бегущих» отметок времени используем ScrollView
и горизонтальный стек HStack
, который будет смещаться по мере изменения временного диапазона rangeTime
.
В TickerView
мы формируем шаг step
, с которым появляются отметки времени TimeMarkView
, основываясь на заданном временном диапазоне rangeTime
и ширине экрана widthRange
…
…, а затем выбираем отметки времени c шагом step
из массива chart.xTime
с помощью индексов indexes
.
Собственно ОСЬ X
— горизонтальную прямую — мы наложим overlay
…
… на горизонтальный стек HStack
, с отметками времени TimeMarkView
, который мы продвигаем с помощью offset
:
Кроме этого, мы можем задавать цвета самой ОСИ X
— colorXAxis
, и отметок — colorXMark
:
YTickerView
— ОСЬ Y
с отметками и сеткой.
Этот View
рисует ОСЬ Y
с цифровыми отметками YMarkView
. Сами отметки YMarkView
представляют собой очень простой View
с вертикальным стеком VStack
, в котором размещены Path
(горизонтальная линия) и Text
с числом:
Совокупность отметок на ОСИ Y
для определенного «набора Графиков» chart
формируется в YTickerView
. Диапазон значений rangeY
вычисляется как объединение диапазонов значений всех «Графиков», входящих в данный «набор Графиков» с помощью функции rangeOfRanges
. Приблизительное количество отметок на ОСИ Y задается параметром estimatedMarksNumber
:
В YTickerView
мы отслеживаем изменение диапазона значений «Графиков» rangeY
. Собственно ОСЬ Y — вертикальную прямую — мы накладываем overlay
на наши отметки…
Кроме этого, мы можем задавать цвета самой ОСИ Y — colorYAxis
, и отметок — colorYMark
:
RangeView
— настройка временного диапазона с помощью «mini-map».
Самой подвижной частью