Работа с графиками в SwiftUI: руководство для начинающих

Когда речь идет о представлении информации пользователям, проще  —  значит лучше.

Особенно когда вы имеете дело с большими наборами данных. У вас есть различные варианты, включая пользовательские представления, таблицы, сводки. Однако можно использовать более впечатляющую форму подачи визуального контента  —  графическое представление.

Графики позволяют пользователям с первого взгляда точно оценить представленные данные.

Это краткое и легко выполнимое руководство по использованию графиков поможет повысить вовлеченность пользователей приложения.

Исходный код доступен здесь

Базовые понятия

График создается путем выстраивания серии элементов. Эти элементы должны соответствовать протоколу ChartContent и представлять типы, которые могут быть выведены в области видимости графика.

Для создания графика используется метод init(content:). В замыкании ViewBuilder добавляются все необходимые визуальные элементы.

struct ChartView: View {
var body: some View {
ChartView {
// Элементы графика
}
}
}

Какие элементы можно использовать?

Во фреймворке Charts есть предустановленный набор готовых к использованию элементов ChartContent, называемых Marks (метками). Каждую метку можно рассматривать как графический элемент, представляющий данные.

Изображение автора

У каждого типа меток есть несколько инициализаторов, выбор которых зависит от того, какой пользовательский интерфейс вы хотите создать.

Можно использовать 3 типа данных в графиках.

  • Количественные: числовые значения, такие как Int (целочисленное), Double (с двойной точностью) и Float (с плавающей точкой).
  • Номинальные: дискретные категории, или группы.
  • Временные: точки во времени.

В зависимости от используемого типа данных, применяются различные конфигурации для управления пользовательским интерфейсом графика.

Перейдем к коду

В целях демонстрации будем работать с визуальным представлением количества кофе, потребляемого пользователями с течением времени и доступного в 4 видах: Latte (латте), Cappuccino (капучино), Cortado (кортадо) и FlatWhite (кофе с молоком),

Для начала создадим простую гистограмму, отражающую общее количество кофе.

struct CoffeeData: Identifiable {
typealias CoffeeDetails = (type: Coffee, amount: Int)
let id = UUID()
let date: Date
let details: [CoffeeDetails]

static func mockData() -> [CoffeeData] { ... }
}

struct DemoChart: View {
@State private var coffeeData = CoffeeData.mockData()

var body: some View {
Chart {
ForEach(coffeeData, id: \.id) { coffeeInfo in
BarMark(
x: .value("Date", coffeeInfo.date),
y: .value("Coffee", totalCoffees(in: coffeeInfo.details))
)
}
}
.frame(height: 300)
.padding()
}

func totalCoffees(in details: [CoffeeData.CoffeeDetails]) -> Int {
return details.map({$0.amount}).reduce(0, +)
}
}
Изображение автора

Кастомизация графика

Чтобы разграничить данные по видам кофе, нужно выполнить дополнительную итерацию через CoffeeDetails и использовать модификатор foregroundStyle(by:) для группирования данных.

struct DemoChart: View {
@State private var coffeeData = CoffeeData.mockData()

var body: some View {
Chart {
ForEach(coffeeData, id: \.id) { coffeeInfo in
ForEach(coffeeInfo.details, id: \.type) { coffeeDetails in
BarMark(
x: .value("Date", coffeeInfo.date),
y: .value("Coffee", coffeeDetails.amount)
)
.foregroundStyle(by: .value("Coffee Type", coffeeDetails.type))
}
}
}
.frame(height: 300)
.padding()
}
}
Изображение автора

Небольшая корректировка позволила получить сгруппированные данные. Однако такой тип графика в основном используется при необходимости отразить прогресс по определенным значениям.

В данном случае надо получить по одному столбику для каждого вида кофе, а это значит, что для каждого значения X (оно же месяц) понадобится 4 столбика (Latte/Cappuccino/Cortado/FlatWhite). Для выполнения этой задачи потребуется внести два изменения.

  • Использовать опцию unit по отношению к значениям оси X, чтобы указать, что надо сгруппировать значения по месяцам.
  • Использовать модификатор position(by:axis:span:), чтобы создать метку для гистограммы.
struct DemoChart: View {
@State private var coffeeData = CoffeeData.mockData()

var body: some View {
Chart {
ForEach(coffeeData, id: \.id) { coffeeInfo in
ForEach(coffeeInfo.details, id: \.type) { coffeeDetails in
BarMark(
x: .value("Date", coffeeInfo.date, unit: .month),
y: .value("Coffee", coffeeDetails.amount)
)
.foregroundStyle(by: .value("Coffee Type", coffeeDetails.type))
.position(by: .value("Coffee Type", coffeeDetails.type))
}
}
}
.frame(height: 300)
.padding()
}
}
Изображение автора

Возможности модификации графиков

Продолжим модифицировать приведенный выше график в соответствии с пользовательскими потребностями.

Пользовательские цвета столбиков

Используйте модификатор chartForegroundStyleScale(_:). Нужно просто присвоить значение каждому группируемому элементу. В данном случае: Latte, Cappuccino, Cortado и FlatWhite.

Изменение масштаба

Если хотите управлять значениями, отображаемыми на оси, чтобы увеличить или уменьшить метки графика, можете использовать модификаторы chartYScale(domain:type:) и chartXScale(domain:type). Диапазон функциональной области может быть замкнутым (например, от 0 до 15) для данных количественного и временного типа или массивом значений для данных дискретного типа.

Настройка меток осей

В данном случае стоит отобразить на оси X месяц вместе с годом, например Aug (Август) 2023. Модификатор chartXAxis(content:) позволяет это сделать.

Добавление аннотаций

Иногда для повышения читаемости следует включать в метки графика дополнительную информацию. Используя annotation(position:alignment:spacing:content), можно поместить любое представление вместе с метками.

struct DemoChart: View {
@State private var coffeeData = CoffeeData.mockData()

var body: some View {
Chart {
ForEach(coffeeData, id: \.id) { coffeeInfo in
ForEach(coffeeInfo.details, id: \.type) { coffeeDetails in
BarMark(
x: .value("Date", coffeeInfo.date, unit: .month),
y: .value("Coffee", coffeeDetails.amount)
)
.annotation(position: .top, alignment: .center) {
Text("\(coffeeDetails.amount)")
}
.foregroundStyle(by: .value("Coffee Type", coffeeDetails.type))
.position(by: .value("Coffee Type", coffeeDetails.type))
.cornerRadius(12)
}
}
}
.chartForegroundStyleScale([
Coffee.latte: Color.accentColor,
Coffee.cappuccino: Color.accentColor.opacity(0.7),
Coffee.cortado: Color.accentColor.opacity(0.5),
Coffee.flatwhite: Color.accentColor.opacity(0.3),
])
.chartXAxis {
AxisMarks(values: .stride(by: .month, count: 1)) { _ in
AxisValueLabel(format: .dateTime.month(.abbreviated).year(.twoDigits), centered: true)
}
}
.chartScrollableAxes(.horizontal)
.chartYScale(domain: 0 ... 15)
.frame(height: 300)
.padding()
}
}
Изображение автора

Комбинирование и интерактивность

Создавая графики, можно добавлять в них различные ChartComponent. Эти компоненты не обязательно должны быть одного типа.

Изображение автора

Чтобы добиться такого пользовательского интерфейса, нужно объединить LineMark и AreaMark.

struct OverallData: Identifiable {
let id = UUID()
let date: Date
let coffee: Int

static func mockData() -> [OverallData] {

return [
.init(date: Date(year: 2023, month: 08), coffee: 12),
.init(date: Date(year: 2023, month: 09), coffee: 15),
.init(date: Date(year: 2023, month: 10), coffee: 8),
.init(date: Date(year: 2023, month: 11), coffee: 18),
.init(date: Date(year: 2023, month: 12), coffee: 14),
.init(date: Date(year: 2024, month: 01), coffee: 22),
]
}
}

struct DemoChart: View {
@State private var overallData = OverallData.mockData()

private var areaBackground: Gradient {
return Gradient(colors: [Color.accentColor, Color.accentColor.opacity(0.1)])
}

var body: some View {
Chart(overallData) {
LineMark(
x: .value("Month", $0.date, unit: .month),
y: .value("Amount", $0.coffee)
)
.symbol(.circle)
.interpolationMethod(.catmullRom)

AreaMark(
x: .value("Month", $0.date, unit: .month),
y: .value("Amount", $0.coffee)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(areaBackground)
}
.chartXAxis {
AxisMarks(values: .stride(by: .month, count: 1)) { _ in
AxisValueLabel(format: .dateTime.month(.abbreviated).year(.twoDigits), centered: true)
}
}
.chartYScale(domain: 0 ... 30)
.frame(height: 300)
.padding()
}
}

Можно к этой комбинации добавить RuleMark вместе с пользовательским представлением в виде Annotation, чтобы каждый пользователь мог выбрать определенную точку для просмотра значения.

Изображение автора
struct DemoChart: View {
@Environment(\.calendar) var calendar
@State private var coffeeData = CoffeeData.mockData()
@State private var overallData = OverallData.mockData()
@State private var chartSelection: Date?

private var areaBackground: Gradient {
return Gradient(colors: [Color.accentColor, Color.accentColor.opacity(0.1)])
}

var body: some View {
Chart(overallData) {
LineMark(
x: .value("Month", $0.date, unit: .month),
y: .value("Amount", $0.coffee)
)
.symbol(.circle)
.interpolationMethod(.catmullRom)

if let chartSelection {
RuleMark(x: .value("Month", chartSelection, unit: .month))
.foregroundStyle(.gray.opacity(0.5))
.annotation(
position: .top,
overflowResolution: .init(x: .fit, y: .disabled)
) {
ZStack {
Text("\(getCoffee(for: chartSelection)) coffees")
}
.padding()
.background {
RoundedRectangle(cornerRadius: 4)
.foregroundStyle(Color.accentColor.opacity(0.2))
}
}
}

AreaMark(
x: .value("Month", $0.date, unit: .month),
y: .value("Amount", $0.coffee)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(areaBackground)
}
.chartXAxis {
AxisMarks(values: .stride(by: .month, count: 1)) { _ in
AxisValueLabel(format: .dateTime.month(.abbreviated).year(.twoDigits), centered: true)
}
}
.chartYScale(domain: 0 ... 30)
.frame(height: 300)
.padding()
.chartXSelection(value: $chartSelection)
}
}

Выводы

  1. Прежде чем использовать графические элементы, подумайте, что вы хотите показать пользователям.
  2. Сосредоточьтесь на моделировании данных. От того, как смоделированы данные, напрямую зависит, насколько легко будет работать с графиком.
  3. Давайте пользователям первое представление о данных, которые они увидят на графике, предоставив информацию в сгруппированном виде. 
  4. Помимо базовых навыков, есть множество других конфигураций и настроек для улучшения графиков. Рекомендую изучить официальную документацию Apple по Charts и посмотреть записи сессий WWDC (Worldwide Developers Conference  —  Всемирная конференция разработчиков, ежегодно проводимая Apple).

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Bruno Lorenzo: Introduction to Charts in SwiftUI

Предыдущая статьяБайт-код JVM: манипулирование и инструментация
Следующая статья10 CSS-однострочников для преобразования веб-приложения