До Swift Charts экосистема визуализации в iOS делилась на три лагеря: Charts (бывший iOS Charts на основе MPAndroidChart), DGCharts и самописные решения. Все они страдали от типичных болячек — устаревшие API, отсутствие декларативной модели, проблемы с производительностью при больших датасетах и слабая интеграция со SwiftUI.
Swift Charts решает эти проблемы за счёт нескольких ключевых решений:
- Декларативность. Вы описываете, какие данные показать, а не как их рисовать. Фреймворк сам решает, как разложить marks на canvas.
- ChartContent protocol. Каждый mark — это
ChartContent, который можно комбинировать через @ChartContentBuilder, как View в SwiftUI.
- Доступность.
.accessibilityLabel и .accessibilityValue работают для каждого mark, плюс automatic audio graphs.
- Адаптивность. Графики масштабируются под Dynamic Type, поддерживают RTL-локали и сами выбирают разумные оси.
Если вам нужна визуализация бизнес-метрик, спортивных данных, финансовых трендов или показателей здоровья — Swift Charts покрывает 95% задач. Для специфичных случаев (orbital diagrams, network graphs, sankey-диаграммы) всё ещё придётся обращаться к Canvas или Metal. Так уж сложилось.
Установка и минимальный пример
Никаких зависимостей — Swift Charts входит в стандартный SDK с iOS 16. Достаточно импортировать модуль:
import SwiftUI
import Charts
struct SalesPoint: Identifiable {
let id = UUID()
let day: String
let revenue: Double
}
struct SimpleBarChart: View {
let data: [SalesPoint] = [
.init(day: "Пн", revenue: 1200),
.init(day: "Вт", revenue: 1850),
.init(day: "Ср", revenue: 1430),
.init(day: "Чт", revenue: 2100),
.init(day: "Пт", revenue: 2890),
.init(day: "Сб", revenue: 3450),
.init(day: "Вс", revenue: 2670)
]
var body: some View {
Chart(data) { point in
BarMark(
x: .value("День", point.day),
y: .value("Выручка", point.revenue)
)
}
.frame(height: 280)
.padding()
}
}
Семь строк кода — и у вас полноценный столбчатый график с осями, сеткой и подписями. Apple рекомендует помечать данные как Identifiable, чтобы фреймворк корректно работал с анимациями при изменениях. На своём опыте: пренебрежение этим однажды стоило мне целого вечера отладки «прыгающих» баров.
Типы графиков (Marks) в Swift Charts
Mark — базовая единица визуализации. В iOS 26 доступны следующие типы.
BarMark — столбчатые диаграммы
Используется для категориальных сравнений. Поддерживает группировку и стекинг через параметр position:
Chart(salesByCategory) { item in
BarMark(
x: .value("Категория", item.category),
y: .value("Продажи", item.amount)
)
.foregroundStyle(by: .value("Регион", item.region))
.position(by: .value("Регион", item.region))
}
Если убрать .position, столбцы будут стекаться. Для горизонтальных диаграмм просто поменяйте местами параметры x и y — всё, никаких отдельных API.
LineMark и AreaMark — линии и области
LineMark идеален для временных рядов, AreaMark — когда важно подчеркнуть объём. Их часто комбинируют:
Chart(temperatureData) { reading in
AreaMark(
x: .value("Время", reading.timestamp),
yStart: .value("Минимум", reading.minTemp),
yEnd: .value("Максимум", reading.maxTemp)
)
.foregroundStyle(.blue.opacity(0.2))
LineMark(
x: .value("Время", reading.timestamp),
y: .value("Среднее", reading.averageTemp)
)
.foregroundStyle(.blue)
.interpolationMethod(.catmullRom)
}
Метод .catmullRom сглаживает кривую — ещё доступны .linear, .monotone, .stepStart, .stepCenter, .stepEnd. Честно говоря, .catmullRom почти всегда выглядит приятнее всего на финансовых данных.
PointMark — scatter plot
Для отображения корреляций и распределений:
Chart(measurements) { m in
PointMark(
x: .value("Высота", m.height),
y: .value("Вес", m.weight)
)
.symbolSize(by: .value("Возраст", m.age))
.foregroundStyle(by: .value("Пол", m.gender))
}
SectorMark — круговые и кольцевые диаграммы (iOS 17+)
До iOS 17 круговые диаграммы приходилось рисовать вручную. Теперь — нет:
Chart(budgetItems) { item in
SectorMark(
angle: .value("Доля", item.amount),
innerRadius: .ratio(0.6),
angularInset: 2
)
.foregroundStyle(by: .value("Категория", item.category))
.cornerRadius(6)
}
.chartLegend(position: .trailing, alignment: .center)
innerRadius: .ratio(0.6) превращает pie в donut. Для классической круговой диаграммы — просто уберите этот параметр.
RuleMark и RectangleMark — линии и прямоугольники
RuleMark рисует горизонтальную или вертикальную линию — отлично подходит для отметки среднего значения, цели или текущего момента:
Chart {
ForEach(data) { point in
LineMark(
x: .value("Дата", point.date),
y: .value("Цена", point.price)
)
}
RuleMark(y: .value("Цель", target))
.foregroundStyle(.green)
.lineStyle(StrokeStyle(lineWidth: 2, dash: [5, 5]))
.annotation(position: .top, alignment: .leading) {
Text("Цель: \(target, format: .currency(code: "RUB"))")
.font(.caption)
.foregroundStyle(.green)
}
}
Кастомизация осей
Автоматические оси хороши для прототипа, но в продакшене почти всегда нужна тонкая настройка. Используйте chartXAxis и chartYAxis:
Chart(monthlyData) { item in
BarMark(
x: .value("Месяц", item.date, unit: .month),
y: .value("Выручка", item.revenue)
)
}
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.month(.abbreviated))
}
}
.chartYAxis {
AxisMarks(position: .leading) { value in
AxisGridLine()
AxisValueLabel {
if let amount = value.as(Double.self) {
Text("\(amount / 1000, specifier: "%.0f")K")
}
}
}
}
Несколько важных деталей:
.stride(by: .month) — указывает шаг тиков по времени. Без него Swift Charts может выбрать слишком плотные подписи (видел собственными глазами, как 365 дней превращались в кашу).
position: .leading — переносит ось Y слева. По умолчанию она справа.
- Кастомные форматы. Внутри
AxisValueLabel можно использовать любой FormatStyle или вручную форматировать текст.
Установка домена осей
Если хотите явно задать диапазон, используйте chartXScale и chartYScale:
.chartYScale(domain: 0...10000)
.chartXScale(domain: startDate...endDate)
Это особенно полезно, когда данные обновляются и вы не хотите, чтобы оси «прыгали» при каждом изменении. Пользователи такое замечают сразу.
Интерактивность: chartXSelection в iOS 17+
До iOS 17 для реализации tap/drag-выбора приходилось писать chartOverlay с DragGesture и руками вычислять координаты через ChartProxy. Кода было — мама не горюй. С iOS 17 появился декларативный модификатор chartXSelection:
struct InteractiveChart: View {
let data: [PricePoint]
@State private var selectedDate: Date?
private var selectedPrice: PricePoint? {
guard let selectedDate else { return nil }
return data.min(by: {
abs($0.date.timeIntervalSince(selectedDate))
< abs($1.date.timeIntervalSince(selectedDate))
})
}
var body: some View {
Chart(data) { point in
LineMark(
x: .value("Дата", point.date),
y: .value("Цена", point.price)
)
.foregroundStyle(.tint)
if let selectedPrice, point.id == selectedPrice.id {
RuleMark(x: .value("Дата", selectedPrice.date))
.foregroundStyle(.gray.opacity(0.3))
.annotation(position: .top, spacing: 0,
overflowResolution: .init(
x: .fit(to: .chart),
y: .disabled
)) {
VStack(alignment: .leading) {
Text(selectedPrice.date, format: .dateTime.day().month())
Text(selectedPrice.price, format: .currency(code: "RUB"))
.font(.headline)
}
.padding(8)
.background(.regularMaterial, in: .rect(cornerRadius: 8))
}
}
}
.chartXSelection(value: $selectedDate)
.frame(height: 320)
}
}
Несколько принципиальных моментов:
chartXSelection работает с любым Plottable-типом — можно биндить Date?, String?, Double?.
overflowResolution предотвращает «вылет» аннотации за границы графика — критично для тултипов у краёв.
- Для выбора диапазона используйте
chartXSelection(range:), который биндит ClosedRange<Plottable>?.
Скроллируемые графики (chartScrollableAxes)
Когда у вас тысячи точек данных, отрисовка всего графика разом убивает FPS. iOS 17 принёс chartScrollableAxes и chartXVisibleDomain:
Chart(yearData) { point in
BarMark(
x: .value("Дата", point.date, unit: .day),
y: .value("Шаги", point.steps)
)
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 3600 * 24 * 30) // 30 дней
.chartScrollPosition(x: $scrollPosition)
.chartScrollTargetBehavior(
.valueAligned(matching: .init(hour: 0), majorAlignment: .matching(.init(weekday: 2)))
)
Параметр chartXVisibleDomain задаёт ширину видимой области в единицах оси (для дат — секунды). chartScrollTargetBehavior делает скролл «прилипающим» к началу недели — UX-приём, заимствованный из приложения «Здоровье». Мелочь, а ощущения совсем другие.
Анимации
Swift Charts автоматически анимирует переходы, если данные обёрнуты в @State и вы оборачиваете изменения в withAnimation:
@State private var data: [SalesPoint] = []
Button("Обновить") {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
data = loadFreshData()
}
}
Для индивидуальной задержки анимации каждого mark используйте .transition:
BarMark(
x: .value("День", point.day),
y: .value("Выручка", point.revenue)
)
.opacity(animateChart ? 1 : 0)
.offset(y: animateChart ? 0 : 50)
Помните: анимация большого количества marks (более ~500) на старых устройствах может проседать. В таких случаях лучше отключать анимацию или применять её только к видимой части через scrollable charts.
Chart3D — трёхмерные графики (iOS 18+)
В iOS 18 Apple представила Chart3D — нативный API для трёхмерной визуализации, который особенно расцвёл в iOS 26 с поддержкой visionOS-стиля интеракций. Это большой шаг вперёд для научных приложений и сложных датасетов:
import Charts
struct Chart3DExample: View {
let data: [Measurement3D]
@State private var pose: Chart3DPose = .default
var body: some View {
Chart3D(data) { point in
PointMark(
x: .value("X", point.x),
y: .value("Y", point.y),
z: .value("Z", point.z)
)
.foregroundStyle(by: .value("Кластер", point.cluster))
.symbolSize(8)
}
.chart3DPose($pose)
.frame(height: 400)
}
}
Привязка chart3DPose позволяет пользователю вращать сцену жестами и сохранять состояние. По умолчанию пользователь может вращать сцену, а вы получаете обратную связь через биндинг.
Когда использовать Chart3D: визуализация научных данных (молекулы, поля, потоки), финансовая аналитика (объёмы по двум измерениям), геопространственные данные. Когда не использовать: бизнес-дашборды и любые случаи, где 2D-проекция читается лучше — 3D часто красив, но менее информативен. Это, к сожалению, частая ошибка дизайнеров: вау-эффект ради вау-эффекта.
Производительность: лучшие практики
Swift Charts прекрасно справляется с тысячами marks, но при десятках тысяч начинаются проблемы. Несколько проверенных приёмов:
- Агрегируйте данные перед рендерингом. Если у вас 100 000 точек за год, сгруппируйте их по дням или часам в зависимости от visible domain.
- Используйте
chartScrollableAxes вместе с chartXVisibleDomain. Apple оптимизирует отрисовку только видимой области.
- Избегайте
id: \.self в ForEach. Лучше делать данные Identifiable с уникальным UUID или стабильным ключом.
- Профилируйте через Instruments. Шаблон SwiftUI Profiler показывает, какие изменения вызывают перерисовку. Очень часто проблема не в Charts, а в том, что родительская View пересчитывается на каждый чих.
- Кэшируйте дорогостоящие
FormatStyle. Не создавайте NumberFormatter внутри AxisValueLabel — выносите в свойство.
Доступность из коробки и ручная тонкая настройка
Swift Charts автоматически генерирует accessibility labels из значений, но качество зависит от вас. Базовый шаблон:
BarMark(
x: .value("День", point.day),
y: .value("Выручка", point.revenue)
)
.accessibilityLabel("\(point.day)")
.accessibilityValue("\(point.revenue, format: .currency(code: "RUB"))")
Для аудио-графиков (audio graphs) добавьте описание серии — VoiceOver проиграет тоны, соответствующие значениям, что особенно полезно слабовидящим пользователям. Это работает «бесплатно», но только если значения корректно типизированы как числа.
Кастомные plot styles и расширения
Через foregroundStyle, symbol и annotation можно собрать практически любой стиль. А если стандартных marks недостаточно, в iOS 17 появился chartOverlay + ChartProxy для рисования поверх осей в координатах графика:
Chart(data) { ... }
.chartOverlay { proxy in
GeometryReader { geo in
Path { path in
let plot = geo[proxy.plotAreaFrame]
path.move(to: CGPoint(x: plot.minX, y: plot.midY))
path.addLine(to: CGPoint(x: plot.maxX, y: plot.midY))
}
.stroke(.red, style: StrokeStyle(lineWidth: 1, dash: [3, 3]))
}
}
ChartProxy также умеет конвертировать координаты touch-событий в значения данных через proxy.value(atX:) — это понадобится для редких случаев, когда chartXSelection не покрывает потребность.
Частые ошибки при работе со Swift Charts
- Передача массива не-
Identifiable объектов. Используйте ForEach(data, id: \.someStableKey) или сделайте тип Identifiable.
- Несовпадение типов в
.value. Все x-значения должны быть одного Plottable-типа. Если смешать String и Date — получите runtime crash. Без вариантов.
- Игнор Dynamic Type. Установка фиксированного шрифта (
.font(.system(size: 10))) ломает доступность. Используйте .font(.caption) и адаптируйтесь к Dynamic Type.
- Тяжёлые computed properties в body. Если данные пересчитываются на каждое обновление родительского состояния, график будет лагать.
FAQ
Чем Swift Charts отличается от сторонних библиотек графиков?
Swift Charts — это нативный SwiftUI-фреймворк от Apple с декларативным API, автоматической поддержкой Dynamic Type, VoiceOver, audio graphs и тёмной темы. Сторонние библиотеки вроде Charts/DGCharts работают через UIKit-обёртки, обновляются медленнее и требуют ручной интеграции с системными API. Если вы пишете под iOS 16+, Swift Charts — стандартный выбор.
Можно ли использовать Swift Charts на macOS, watchOS и tvOS?
Да. Фреймворк поддерживает iOS 16+, macOS 13+, watchOS 9+ и tvOS 16+. Некоторые модификаторы (например, chartScrollableAxes) требуют iOS 17+, а Chart3D — iOS 18+. На watchOS 9 поддерживаются только базовые типы marks без интерактивного выбора, но в watchOS 11+ функциональность близка к iPhone.
Как сделать кастомный tooltip при наведении на точку?
Используйте chartXSelection(value:) с биндингом, найдите ближайшую точку к выбранному значению и добавьте RuleMark с .annotation. Параметр overflowResolution предотвратит вылет тултипа за границы графика. Полный пример приведён в разделе про интерактивность выше.
Поддерживает ли Swift Charts экспорт в PNG или PDF?
Напрямую — нет, но любую SwiftUI View можно отрендерить в изображение через ImageRenderer (iOS 16+). Создайте инстанс ImageRenderer(content: yourChart), обратитесь к uiImage или cgImage, и сохраните результат через UIImage.pngData(). Для PDF используйте renderer.render { size, context in ... }.
Что выбрать: Swift Charts или Canvas с Path для своего графика?
Используйте Swift Charts для всех стандартных типов диаграмм (столбчатые, линейные, точечные, круговые, area, scatter) — это сэкономит сотни строк кода и обеспечит доступность. Переходите на Canvas/Path только для нестандартных визуализаций: sankey-диаграмм, network graphs, узлов orbit-схем или сложных кастомных шкал. Можно и комбинировать — основной график на Swift Charts, а кастомные элементы поверх через chartOverlay.
Заключение
Swift Charts в iOS 26 покрывает практически весь спектр задач визуализации данных в нативных приложениях Apple. От простых столбчатых диаграмм за пять строк до интерактивных скроллируемых временных рядов и трёхмерной визуализации с Chart3D — всё это доступно через декларативный SwiftUI-DSL. Главное, что вы получаете в подарок: автоматическую доступность, корректную работу с Dynamic Type, плавные анимации и интеграцию с тёмной темой.
Дальнейшие шаги: попробуйте перенести один из текущих графиков вашего приложения на Swift Charts, измерьте разницу в строках кода и FPS на больших датасетах. В большинстве случаев результат приятно удивит — лично у меня после миграции одного дашборда на Swift Charts кодовая база похудела примерно на 40%.