Лучший опыт

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

Когда речь идет о представлении информации пользователям, проще  —  значит лучше. Особенно когда вы имеете дело с большими наборами данных. У вас есть различные варианты, включая пользовательские представления, таблицы, сводки. Однако можно использовать более впечатляющую форму подачи визуального контента  —  графическое представление. Графики позволяют пользователям с первого взгляда точно оценить представленные данные. Э
Работа с графиками в 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).