Swift Charts в SwiftUI: создание графиков и визуализация данных в iOS 26

Полное руководство по Swift Charts в SwiftUI для iOS 26: типы графиков, кастомизация осей, интерактивный выбор, скроллируемые графики, 3D-визуализация Chart3D, анимации и оптимизация производительности с реальными примерами кода Swift 6.

Swift Charts iOS 26: Chart3D и графики SwiftUI

Swift Charts — нативный фреймворк Apple для построения графиков в SwiftUI. Появился он ещё в iOS 16 и за эти годы заметно подрос — к iOS 26 это уже совсем другой инструмент. Помню, как раньше для визуализации данных приходилось тащить сторонние библиотеки или писать всё на Path и GeometryReader (и да, это было больно). Сегодня же вы получаете декларативный DSL, поддержку Dynamic Type, VoiceOver, локализацию и автоматическую адаптацию под тёмную тему — буквально из коробки.

В этом руководстве разберём Swift Charts от базовых столбчатых диаграмм до интерактивного выбора, скроллируемых временных рядов, 3D-визуализации с Chart3D (iOS 18+) и тонкостей кастомизации осей. Все примеры написаны под Swift 6 и протестированы в Xcode 16.4 на iOS 26.

Зачем нужен Swift Charts и когда его выбирать

До 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)
    }
}

Несколько принципиальных моментов:

  1. chartXSelection работает с любым Plottable-типом — можно биндить Date?, String?, Double?.
  2. overflowResolution предотвращает «вылет» аннотации за границы графика — критично для тултипов у краёв.
  3. Для выбора диапазона используйте 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, но при десятках тысяч начинаются проблемы. Несколько проверенных приёмов:

  1. Агрегируйте данные перед рендерингом. Если у вас 100 000 точек за год, сгруппируйте их по дням или часам в зависимости от visible domain.
  2. Используйте chartScrollableAxes вместе с chartXVisibleDomain. Apple оптимизирует отрисовку только видимой области.
  3. Избегайте id: \.self в ForEach. Лучше делать данные Identifiable с уникальным UUID или стабильным ключом.
  4. Профилируйте через Instruments. Шаблон SwiftUI Profiler показывает, какие изменения вызывают перерисовку. Очень часто проблема не в Charts, а в том, что родительская View пересчитывается на каждый чих.
  5. Кэшируйте дорогостоящие 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%.

Об авторе Daniel Okafor

Daniel is a former Spotify iOS engineer (2019-2024) who worked on the Now Playing surface and the in-app podcast player. He shipped the SwiftUI rewrite of the lyrics view to over 600 million users and contributed several fixes upstream to swift-collections. His writing tends toward the unglamorous corners of iOS work: build-time regressions in Xcode 16, why SwiftData still isn't ready for production sync scenarios, and how to instrument a real app with os_signpost without drowning in noise. He spent two years before Spotify at a fintech startup in Berlin building a banking app on top of Solaris API. Daniel now freelances out of Lisbon and maintains a small open-source library for type-safe deep links in SwiftUI. He has 9 years of native iOS experience.