Работа с графиками в 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) } }
Выводы
- Прежде чем использовать графические элементы, подумайте, что вы хотите показать пользователям.
- Сосредоточьтесь на моделировании данных. От того, как смоделированы данные, напрямую зависит, насколько легко будет работать с графиком.
- Давайте пользователям первое представление о данных, которые они увидят на графике, предоставив информацию в сгруппированном виде.
- Помимо базовых навыков, есть множество других конфигураций и настроек для улучшения графиков. Рекомендую изучить официальную документацию Apple по Charts и посмотреть записи сессий WWDC (Worldwide Developers Conference — Всемирная конференция разработчиков, ежегодно проводимая Apple).