Swift Charts 完全指南:从基础图表到 iOS 26 的 3D 数据可视化

从柱状图、折线图到 iOS 26 全新 3D 图表,全面掌握 Swift Charts 框架。包含 BarMark、LineMark、SectorMark、Chart3D、SurfacePlot 的完整代码示例与实用技巧。

说实话,在 Swift Charts 出现之前,iOS 上做数据可视化真的挺折腾的。要么用第三方库(比如 Charts/Daniel Gindi),要么自己用 Core Graphics 从零开始画。前者跟 SwiftUI 的集成总是差点意思,后者嘛……你得写一大堆底层代码,光想想就头疼。

转折点出现在 2022 年的 WWDC。Apple 正式推出了 Swift Charts——一个原生的、声明式的图表框架,跟 SwiftUI 深度集成。从 iOS 16 开始可用,它让你几行代码就能搞出好看、可访问、性能又好的图表。

而且 Apple 一直在加码。iOS 17 加了扇形图(SectorMark)和可滚动图表,到了 2025 年 WWDC 发布的 iOS 26,更是直接上了 3D 图表,把数据可视化拉到了一个全新的维度。说真的,第一次看到 Chart3D 的 demo 时,我还是挺震撼的。

这篇文章会从 Swift Charts 的基础概念开始,一步步深入到各种图表类型、交互、滚动、坐标轴定制,最后到 iOS 26 全新的 3D 可视化。不管你是刚入门的新手还是想了解 3D 特性的老手,应该都能找到有用的东西。

开始使用 Swift Charts

好消息是,Swift Charts 内置在 iOS 16+ 的 SDK 里,不用装任何第三方依赖。导入框架就能开始用:

import SwiftUI
import Charts

核心概念其实很简单——用一个 Chart 容器视图,往里面放标记(Mark)就行。每种标记代表一种数据的可视化方式。框架提供了 BarMark、LineMark、AreaMark、PointMark、RuleMark、SectorMark 这些标记类型,基本覆盖了常见需求。

在画图表之前,先定义好数据模型。Swift Charts 跟 Identifiable 协议配合得很好:

struct SalesData: Identifiable {
    let id = UUID()
    let month: String
    let revenue: Double
    let category: String
}

let sampleData: [SalesData] = [
    SalesData(month: "1月", revenue: 5000, category: "电子产品"),
    SalesData(month: "2月", revenue: 8000, category: "电子产品"),
    SalesData(month: "3月", revenue: 7500, category: "电子产品"),
    SalesData(month: "1月", revenue: 3000, category: "服装"),
    SalesData(month: "2月", revenue: 4500, category: "服装"),
    SalesData(month: "3月", revenue: 6000, category: "服装"),
]

有了数据模型,就可以开始画各种图表了。

BarMark — 柱状图

柱状图大概是最常见的图表类型了,非常适合做分类数据的比较。在 Swift Charts 里用 BarMark 就能搞定。

基础柱状图

struct BasicBarChart: View {
    let data: [SalesData]

    var body: some View {
        Chart(data) { item in
            BarMark(
                x: .value("月份", item.month),
                y: .value("收入", item.revenue)
            )
        }
        .frame(height: 300)
    }
}

就这么几行,一个基础柱状图就出来了。

分组柱状图

想按类别分组显示?加上 foregroundStyle(by:) 自动着色,再配合 position(by:) 让柱子并排排列(而不是堆叠):

struct GroupedBarChart: View {
    let data: [SalesData]

    var body: some View {
        Chart(data) { item in
            BarMark(
                x: .value("月份", item.month),
                y: .value("收入", item.revenue)
            )
            .foregroundStyle(by: .value("类别", item.category))
            .position(by: .value("类别", item.category))
        }
        .frame(height: 300)
    }
}

堆叠柱状图与样式定制

去掉 position(by:) 的话,同组数据会自动堆叠。你还可以用 clipShapecornerRadius 让柱子更好看:

struct StyledBarChart: View {
    let data: [SalesData]

    var body: some View {
        Chart(data) { item in
            BarMark(
                x: .value("月份", item.month),
                y: .value("收入", item.revenue),
                width: .ratio(0.6)
            )
            .foregroundStyle(by: .value("类别", item.category))
            .clipShape(RoundedRectangle(cornerRadius: 6))
        }
        .chartForegroundStyleScale([
            "电子产品": Color.blue,
            "服装": Color.orange
        ])
        .frame(height: 300)
    }
}

BarMarkwidth 参数支持 .ratio().fixed().automatic 这几种选项,可以精确控制柱子宽度。chartForegroundStyleScale 则用来自定义类别的颜色映射——这个在实际项目中特别有用。

LineMark — 折线图

折线图最适合展示数据随时间的变化趋势了。LineMark 就是干这个的。

基础折线图与多条折线

struct TemperatureData: Identifiable {
    let id = UUID()
    let date: Date
    let temperature: Double
    let city: String
}

struct MultiLineChart: View {
    let data: [TemperatureData]

    var body: some View {
        Chart(data) { item in
            LineMark(
                x: .value("日期", item.date),
                y: .value("温度", item.temperature)
            )
            .foregroundStyle(by: .value("城市", item.city))
            .symbol(by: .value("城市", item.city))
        }
        .frame(height: 300)
    }
}

插值方法与线条样式

Swift Charts 提供了好几种插值方法来控制折线的形态。.monotone 给你平滑但保持单调性的曲线,.cardinal 更圆润一些,.catmullRom 则生成经过所有数据点的平滑样条曲线。线条宽度和虚线样式也都能自定义:

struct StyledLineChart: View {
    let data: [TemperatureData]

    var body: some View {
        Chart(data) { item in
            LineMark(
                x: .value("日期", item.date),
                y: .value("温度", item.temperature)
            )
            .interpolationMethod(.catmullRom)
            .lineStyle(StrokeStyle(
                lineWidth: 2.5,
                dash: [8, 4]
            ))
            .foregroundStyle(.blue.gradient)
        }
        .frame(height: 300)
    }
}

另外,.symbol() 修饰符可以在数据点上加标记符号,内置了 .circle.square.triangle.diamond.cross 这些形状。

AreaMark — 面积图

面积图本质上就是填充版的折线图,能更直观地感受数据的体量。AreaMark 还支持渐变填充和范围展示,效果还挺漂亮的。

基础面积图与渐变填充

struct GradientAreaChart: View {
    let data: [SalesData]

    var body: some View {
        Chart(data) { item in
            AreaMark(
                x: .value("月份", item.month),
                y: .value("收入", item.revenue)
            )
            .foregroundStyle(
                .linearGradient(
                    colors: [.blue.opacity(0.6), .blue.opacity(0.1)],
                    startPoint: .top,
                    endPoint: .bottom
                )
            )
            .interpolationMethod(.catmullRom)
        }
        .frame(height: 300)
    }
}

范围面积图

yStartyEnd 参数可以画范围面积图,特别适合展示数据的上下界或者置信区间:

struct RangeData: Identifiable {
    let id = UUID()
    let month: String
    let minTemp: Double
    let maxTemp: Double
}

struct RangeAreaChart: View {
    let data: [RangeData]

    var body: some View {
        Chart(data) { item in
            AreaMark(
                x: .value("月份", item.month),
                yStart: .value("最低温", item.minTemp),
                yEnd: .value("最高温", item.maxTemp)
            )
            .foregroundStyle(.green.opacity(0.3))
            .interpolationMethod(.monotone)
        }
        .frame(height: 300)
    }
}

PointMark — 散点图

散点图用来展示两个变量之间的关系。PointMark 可以单独用,也可以跟 LineMark 组合——后者在实际项目中其实更常见。

struct StudentScore: Identifiable {
    let id = UUID()
    let studyHours: Double
    let examScore: Double
    let subject: String
}

struct ScatterPlotView: View {
    let scores: [StudentScore]

    var body: some View {
        Chart(scores) { item in
            PointMark(
                x: .value("学习时长", item.studyHours),
                y: .value("考试成绩", item.examScore)
            )
            .foregroundStyle(by: .value("科目", item.subject))
            .symbolSize(80)
            .symbol(by: .value("科目", item.subject))
        }
        .chartXAxisLabel("每日学习时长(小时)")
        .chartYAxisLabel("考试成绩(分)")
        .frame(height: 350)
    }
}

PointMarkLineMark 搭配使用,可以在折线上清楚地标出每个数据点:

Chart(data) { item in
    LineMark(
        x: .value("月份", item.month),
        y: .value("收入", item.revenue)
    )
    PointMark(
        x: .value("月份", item.month),
        y: .value("收入", item.revenue)
    )
    .symbolSize(60)
}

RuleMark — 参考线

RuleMark 用来在图表上画参考线——目标值、平均值、阈值这些。别小看这个功能,它能让用户一眼就看出数据有没有达标。

struct ChartWithThreshold: View {
    let data: [SalesData]
    let target: Double = 6000

    var body: some View {
        Chart {
            ForEach(data) { item in
                BarMark(
                    x: .value("月份", item.month),
                    y: .value("收入", item.revenue)
                )
                .foregroundStyle(.blue.opacity(0.8))
            }

            RuleMark(y: .value("目标", target))
                .foregroundStyle(.red)
                .lineStyle(StrokeStyle(lineWidth: 2, dash: [6, 3]))
                .annotation(position: .top, alignment: .leading) {
                    Text("目标: ¥\(Int(target))")
                        .font(.caption)
                        .foregroundStyle(.red)
                }
        }
        .frame(height: 300)
    }
}

RuleMark 也能画垂直参考线(用 x 参数就行)。.annotation() 修饰符可以在参考线旁边加文字标注,position 参数控制标注在哪个方向。

SectorMark — 扇形图(iOS 17+)

iOS 17 终于带来了原生的饼图和环形图支持!说「终于」是因为这真的是很多开发者等了好久的功能。SectorMark 用起来也很直观。

饼图与环形图

struct ExpenseCategory: Identifiable {
    let id = UUID()
    let name: String
    let amount: Double
}

struct DonutChartView: View {
    let expenses: [ExpenseCategory] = [
        ExpenseCategory(name: "餐饮", amount: 3200),
        ExpenseCategory(name: "交通", amount: 1500),
        ExpenseCategory(name: "娱乐", amount: 2100),
        ExpenseCategory(name: "购物", amount: 4800),
        ExpenseCategory(name: "其他", amount: 1200),
    ]

    var body: some View {
        Chart(expenses) { item in
            SectorMark(
                angle: .value("金额", item.amount),
                innerRadius: .ratio(0.6),
                angularInset: 2
            )
            .cornerRadius(5)
            .foregroundStyle(by: .value("类别", item.name))
        }
        .frame(height: 350)
    }
}

几个关键参数值得说一下:innerRadius 设成 .ratio(0.6) 就变成环形图了(内径是外径的 60%),设成零就是标准饼图;angularInset 在扇区间加间距,视觉上更清爽;cornerRadius 给扇区加圆角,看起来更精致。

交互式扇形图

通过 chartAngleSelection,用户可以点击或拖动来选择扇区:

struct InteractiveDonutChart: View {
    let expenses: [ExpenseCategory]
    @State private var selectedAngle: Double?

    var selectedItem: ExpenseCategory? {
        guard let selectedAngle else { return nil }
        var cumulative: Double = 0
        for expense in expenses {
            cumulative += expense.amount
            if selectedAngle <= cumulative {
                return expense
            }
        }
        return nil
    }

    var body: some View {
        VStack {
            Chart(expenses) { item in
                SectorMark(
                    angle: .value("金额", item.amount),
                    innerRadius: .ratio(0.6),
                    angularInset: 2
                )
                .cornerRadius(5)
                .foregroundStyle(by: .value("类别", item.name))
                .opacity(selectedItem?.name == item.name ? 1.0 : 0.6)
            }
            .chartAngleSelection(value: $selectedAngle)
            .frame(height: 300)

            if let selected = selectedItem {
                Text("\(selected.name): ¥\(Int(selected.amount))")
                    .font(.headline)
            }
        }
    }
}

图表交互

Swift Charts 的交互功能做得相当不错,图表不只是静态展示,还能让用户主动探索数据。

选择与悬停

chartXSelectionchartYSelection 让用户在图表上做选择操作。配合 chartOverlay,你可以实现类似 tooltip 的效果:

struct InteractiveLineChart: View {
    let data: [SalesData]
    @State private var selectedMonth: String?

    var body: some View {
        Chart(data) { item in
            LineMark(
                x: .value("月份", item.month),
                y: .value("收入", item.revenue)
            )
            .interpolationMethod(.catmullRom)

            if let selectedMonth, item.month == selectedMonth {
                RuleMark(x: .value("选中", selectedMonth))
                    .foregroundStyle(.gray.opacity(0.3))
                    .lineStyle(StrokeStyle(lineWidth: 1))
                    .annotation(
                        position: .top,
                        overflowResolution: .init(
                            x: .fit(to: .chart),
                            y: .disabled
                        )
                    ) {
                        VStack(alignment: .leading, spacing: 4) {
                            Text(item.month)
                                .font(.caption)
                                .foregroundStyle(.secondary)
                            Text("¥\(Int(item.revenue))")
                                .font(.headline)
                        }
                        .padding(8)
                        .background(.ultraThinMaterial)
                        .cornerRadius(8)
                    }
            }
        }
        .chartXSelection(value: $selectedMonth)
        .frame(height: 300)
    }
}

如果 X 轴用的是 Date 类型,绑定值类型就改成 Date?。框架会自动帮你匹配最近的数据点,交互动画也是内置的,不用额外配置。体验非常丝滑。

可滚动图表

数据点多的时候,挤在一个画面里看着实在太拥挤。iOS 17 引入了可滚动图表,用户可以左右滑动来浏览更长时间跨度的数据。这个功能在健康、运动、金融类 App 里特别实用。

struct DailyStep: Identifiable {
    let id = UUID()
    let date: Date
    let steps: Int
}

struct ScrollableChartView: View {
    let dailySteps: [DailyStep]
    @State private var scrollPosition: Date

    init(dailySteps: [DailyStep]) {
        self.dailySteps = dailySteps
        _scrollPosition = State(
            initialValue: dailySteps.last?.date ?? .now
        )
    }

    var body: some View {
        Chart(dailySteps) { item in
            BarMark(
                x: .value("日期", item.date, unit: .day),
                y: .value("步数", item.steps)
            )
            .foregroundStyle(.green.gradient)
        }
        .chartScrollableAxes(.horizontal)
        .chartXVisibleDomain(length: 3600 * 24 * 7)
        .chartScrollPosition(x: $scrollPosition)
        .chartScrollTargetBehavior(
            .valueAligned(
                matching: DateComponents(weekday: 2),
                majorAlignment: .matching(
                    DateComponents(day: 1)
                )
            )
        )
        .frame(height: 300)
    }
}

简单解释一下关键部分:chartScrollableAxes(.horizontal) 启用水平滚动;chartXVisibleDomain(length:) 设置可见窗口大小,这里是 7 天;chartScrollPosition 双向绑定滚动位置;chartScrollTargetBehavior 控制滚动对齐行为,让滚动停下来时自动对齐到周一或月初。

坐标轴与图例定制

Swift Charts 的坐标轴和图例都可以高度自定义。如果你对默认样式不满意(虽然默认的已经挺好了),可以这样来调整:

struct CustomizedAxisChart: View {
    let data: [SalesData]

    var body: some View {
        Chart(data) { item in
            BarMark(
                x: .value("月份", item.month),
                y: .value("收入", item.revenue)
            )
            .foregroundStyle(by: .value("类别", item.category))
        }
        .chartXAxis {
            AxisMarks(values: .automatic) { value in
                AxisGridLine()
                    .foregroundStyle(.gray.opacity(0.3))
                AxisTick()
                    .foregroundStyle(.gray)
                AxisValueLabel()
                    .font(.caption)
                    .foregroundStyle(.primary)
            }
        }
        .chartYAxis {
            AxisMarks(position: .leading) { value in
                AxisGridLine(
                    stroke: StrokeStyle(dash: [4, 4])
                )
                AxisValueLabel {
                    if let intValue = value.as(Int.self) {
                        Text("¥\(intValue)")
                            .font(.caption2)
                    }
                }
            }
        }
        .chartLegend(position: .bottom, spacing: 20)
        .frame(height: 300)
    }
}

chartXAxischartYAxis 闭包里,你可以完全掌控坐标轴的外观。AxisMarks 下面能自定义刻度线、网格线和标签,每个组件都支持独立设置样式。

chartLegend 控制图例的位置。可以放在 .top.bottom.leading.trailing,也可以用 .hidden 完全隐藏图例(然后自己做一个自定义版本)。

Chart3D — iOS 26 全新 3D 图表

接下来是最让人兴奋的部分了。

iOS 26(WWDC 2025)给 Swift Charts 带来了最大的一次更新——三维图表。全新的 Chart3D 容器让你能在三维空间里可视化数据,支持 iPhone、iPad、Mac 和 Apple Vision Pro。

这可不只是个炫酷的噱头。对于科学计算、工程分析、金融建模这类需要展示多维数据关系的场景,3D 图表有着很高的实用价值。

基础 3D 散点图

先从一个三维散点图开始吧。Chart3D 里的 PointMark 现在支持 z 轴参数了,可以把数据点投射到三维空间:

struct Particle: Identifiable {
    let id = UUID()
    let x: Double
    let y: Double
    let z: Double
    let energy: Double
    let type: String
}

struct ScatterPlot3DView: View {
    let particles: [Particle] = {
        var result: [Particle] = []
        for _ in 0..<200 {
            result.append(Particle(
                x: Double.random(in: -10...10),
                y: Double.random(in: -10...10),
                z: Double.random(in: -10...10),
                energy: Double.random(in: 0...100),
                type: ["Alpha", "Beta", "Gamma"].randomElement()!
            ))
        }
        return result
    }()

    var body: some View {
        Chart3D(particles) { particle in
            PointMark(
                x: .value("X 坐标", particle.x),
                y: .value("能量", particle.energy),
                z: .value("Z 坐标", particle.z)
            )
            .foregroundStyle(by: .value("类型", particle.type))
            .symbolSize(particle.energy * 2)
        }
        .chartXAxisLabel("X 轴")
        .chartYAxisLabel("能量 (eV)")
        .chartZAxisLabel("Z 轴")
        .frame(height: 450)
    }
}

这段代码创建了一个粒子分布的 3D 散点图。每个粒子在三维空间中定位,大小由能量值决定,颜色代表粒子类型。chartZAxisLabel 是 iOS 26 新增的修饰符。

SurfacePlot — 三维曲面图

SurfacePlot 是 iOS 26 引入的全新标记类型,用来渲染数学曲面。如果你需要可视化函数关系、地形数据、或者任何能用 f(x, z) 表达的东西,这个就是你要的。

struct MathSurfaceView: View {
    var body: some View {
        Chart3D {
            SurfacePlot(
                x: "x",
                y: "y",
                z: "z"
            ) { x, z in
                sin(x) * cos(z)
            }
            .xDomain(-Double.pi ... Double.pi)
            .zDomain(-Double.pi ... Double.pi)
            .foregroundStyle(
                .linearGradient(
                    colors: [.blue, .cyan, .green, .yellow, .red],
                    startPoint: .bottom,
                    endPoint: .top
                )
            )
        }
        .chartYAxisLabel("y = sin(x) · cos(z)")
        .chartXAxisLabel("x")
        .chartZAxisLabel("z")
        .chart3DProjection(.perspective)
        .frame(height: 500)
    }
}

这个例子渲染了经典的 sin(x) * cos(z) 曲面。闭包接收 x 和 z 坐标,返回 y 值。xDomainzDomain 定义定义域范围。那个彩虹渐变色映射让高低起伏一目了然,效果真的很赞。

相机投影与视角控制

3D 图表的一大看点就是相机控制。iOS 26 提供了两种投影方式和灵活的视角调整:

struct Interactive3DChartView: View {
    @State private var pose = Chart3DPose(
        azimuth: .degrees(45),
        inclination: .degrees(30)
    )

    var body: some View {
        Chart3D(particles) { particle in
            PointMark(
                x: .value("X", particle.x),
                y: .value("Y", particle.y),
                z: .value("Z", particle.z)
            )
            .foregroundStyle(by: .value("类型", particle.type))
        }
        .chart3DProjection(.perspective)
        .chart3DPose($pose)
        .frame(height: 500)
    }
}

两种投影模式各有用途:.perspective(透视投影)模拟人眼效果,远的物体看起来更小,有空间深度感;.orthographic(正交投影)没有近大远小,所有物体保持相同比例,更适合精确的科学分析。

Chart3DPose 控制观察视角——azimuth 是水平旋转角度,inclination 是垂直倾斜角度。作为 Binding 传入后,用户可以通过拖动手势旋转 3D 图表。在 Apple Vision Pro 上就更自然了,直接用手势旋转和缩放。

说到 Vision Pro,Chart3D 还支持体积窗口(volumetric window)渲染,3D 图表可以真正悬浮在用户面前的空间中。光想想就觉得酷。

最佳实践与性能优化

聊完了各种图表类型和花哨的 3D 功能,来说说实际项目中需要注意的事情。

数据优化

  • 控制数据点数量:折线图和面积图如果数据点超过几千个,应该考虑降采样。LTTB(Largest Triangle Three Buckets)算法是个不错的选择,能在保持数据特征的同时减少点数。
  • 用值类型:尽量用 struct 而不是 class 来定义数据模型,这对 SwiftUI 的 diff 机制更友好。
  • 别在 body 里做计算:数据的筛选、排序、聚合这些操作要放在 body 外面,用 @State@StateObject 或计算好的属性来存储。这个坑我见过不少人踩。

可访问性

  • Swift Charts 内置了 VoiceOver 支持,会自动生成无障碍描述。可以通过 .accessibilityLabel().accessibilityValue() 自定义描述。
  • Audio Graphs 功能让视力障碍用户可以通过声音来「听到」数据趋势变化——这是内置功能,不用额外写代码。
  • 选颜色时记得考虑色盲用户。chartForegroundStyleScale 配合系统无障碍色板是个好做法。

设计与色彩

  • 遵循平台规范:Swift Charts 默认用系统色彩,自动适应浅色和深色模式。没有特殊需求的话,默认配色其实就挺好。
  • 保持简洁:一个图表里别放太多数据系列。3-5 个是比较合理的上限,再多就考虑用筛选器或者分页吧。
  • 动画要克制.animation() 可以给数据变化加过渡动画,但别让动画喧宾夺主,影响用户理解数据。

性能建议

  1. 数据点很多的图表,用 chartPlotStyle 设置固定尺寸,避免不必要的布局重算。
  2. 滚动图表中,框架会自动管理屏幕外数据的渲染,你不用手动做虚拟化。
  3. 如果图表需要频繁更新(比如实时数据),考虑用 TimelineView 配合固定的刷新间隔来控制更新频率。
  4. Chart3D 的 3D 渲染比 2D 消耗更多资源。在老设备上记得测试,必要时减少数据点或降低曲面分辨率。

总结

Swift Charts 从 iOS 16 首次亮相到 iOS 26 的 3D 图表,走过了一段很漂亮的进化之路:

  • iOS 16(2022):框架首发,带来了 BarMark、LineMark、AreaMark、PointMark、RuleMark、RectangleMark,奠定了声明式图表的基础。
  • iOS 17(2023):新增 SectorMark、可滚动图表、交互增强,大幅扩展了能力范围。
  • iOS 18(2024):细节优化和性能提升,让框架更加稳定可靠。
  • iOS 26(2025):Chart3D 和 SurfacePlot 横空出世,把数据可视化从 2D 带到了 3D,还为 Vision Pro 提供了原生支持。

Swift Charts 的设计理念很好地体现了 Apple 的产品哲学:简单的事情应该简单,复杂的事情应该可能。几行代码就能出一个好看的图表,同时通过丰富的修饰符和选项,你也能做出高度定制化的可视化作品。

展望未来,随着 Vision Pro 生态的成熟,3D 图表的交互方式会变得更丰富。ML 与可视化的结合也值得期待——比如图表自动识别异常值并高亮,或者根据数据特征推荐最佳图表类型。

不管你在做金融 App、健康追踪、数据仪表盘还是科学计算工具,Swift Charts 都能用最少的代码量、最大的表达力来讲好数据的故事。从基础柱状图到 3D 曲面可视化,工具箱已经备好了——现在就开始用吧。

关于作者 Editorial Team

Our team of expert writers and editors.