WWDC Quick Look 💓 By SwiftGGTeam
Swift Charts: Raise the bar

Swift Charts: Raise the bar

观看原视频

Highlight

Swift Charts 在基本图表之外提供了丰富的 Mark 组合、自定义坐标轴样式、plot area 背景和手势交互能力,让开发者可以构建从简单到复杂的完整数据可视化体验。


核心内容

给 App 加上一个柱状图或折线图并不难,Hello Swift Charts 的 session 已经演示了基本用法。但真正上线一个数据看板时,你会遇到一堆问题:如何在同一张图里组合多种视觉元素?如何让 X 轴标签按季度分组?如何让用户用手指拖拽选择数据范围?

这些需求在之前需要自己写 Core Graphics 或引入第三方库。Swift Charts 在基础 Mark 之上提供了一套组合与定制机制,让复杂图表的实现成本大幅降低。

这个 session 从零开始,用同一个煎饼销售数据集,逐步叠加复杂度——从单 Mark 到多 Mark 叠加,从默认样式到完全自定义坐标轴,再到拖拽交互。整个过程展示了 Swift Charts 从”能用”到”好用”的完整路径。


详细内容

组合多种 Mark 表达丰富数据

3:48)横向柱状图是展示排名数据的经典选择。交换 x 和 y 的值即可得到横向布局:

Chart(data, id: \.name) {
    BarMark(
        x: .value("Sales", $0.sales),
        y: .value("Name", $0.name)
    )
    .foregroundStyle(.pink)
    .accessibilityLabel($0.name)
    .accessibilityValue("\($0.sales) sold")
}

关键点:

  • BarMark 的 x 放数值、y 放类别,自动渲染为横向柱状图
  • .foregroundStyle(.pink) 统一设置颜色
  • .accessibilityLabel.accessibilityValue 为 VoiceOver 提供定制描述

5:12)对于时序数据,用 LineMark 替换 BarMark 只需改一个类型名:

Chart {
    ForEach(dailySales, id: \.day) {
        BarMark(
            x: .value("Day", $0.day, unit: .day),
            y: .value("Sales", $0.sales)
        )
    }
}

关键点:

  • unit: .day 告诉框架数据的时间粒度,自动生成合适的轴标签
  • BarMark 改为 LineMark 即可切换图表类型,数据层不需要任何改动

6:16)多城市对比时,LineMark 配合 .foregroundStyle(by:) 自动为不同系列分配颜色:

Chart {
    ForEach(seriesData, id: \.city) { series in
        ForEach(series.data, id: \.weekday) {
            LineMark(
                x: .value("Weekday", $0.weekday, unit: .day),
                y: .value("Sales", $0.sales)
            )
        }
        .foregroundStyle(by: .value("City", series.city))
        .symbol(by: .value("City", series.city))
        .interpolationMethod(.catmullRom)
    }
}

关键点:

  • 嵌套 ForEach 处理多系列数据:外层遍历城市,内层遍历该城市的每日数据
  • .foregroundStyle(by:) 按城市分组着色,自动生成图例
  • .symbol(by:) 为不同系列使用不同数据点标记
  • .interpolationMethod(.catmullRom) 使用 Catmull-Rom 曲线平滑连线

7:19)改成 BarMark 进行分组柱状图对比时,关键区别是 .position(by:)

BarMark(
    x: .value("Weekday", $0.weekday, unit: .day),
    y: .value("Sales", $0.sales)
)
.foregroundStyle(by: .value("City", series.city))
.position(by: .value("City", series.city))

关键点:

  • .position(by:) 让同一 x 位置的柱子按城市并排显示
  • 不加 .position(by:) 时柱子会堆叠在一起
  • LineMark 切换到 BarMark 只需改类型名并加上 .position(by:)

8:02)在同一张图中叠加多种 Mark,可以同时展示数据范围和平均值:

Chart {
    ForEach(data, id: \.month) {
        AreaMark(
            x: .value("Month", $0.month, unit: .month),
            yStart: .value("Daily Min", $0.dailyMin),
            yEnd: .value("Daily Max", $0.dailyMax)
        )
        .opacity(0.3)
        LineMark(
            x: .value("Month", $0.month, unit: .month),
            y: .value("Daily Average", $0.dailyAverage)
        )
    }
}

关键点:

  • AreaMarkyStartyEnd 定义区域范围,展示每月最低到最高销量区间
  • LineMark 叠加在区域上方,展示每日平均值趋势
  • .opacity(0.3) 让区域半透明,确保折线清晰可见

8:46)另一种组合方式:BarMark + RectangleMark

BarMark(
    x: .value("Month", $0.month, unit: .month),
    yStart: .value("Daily Min", $0.dailyMin),
    yEnd: .value("Daily Max", $0.dailyMax),
    width: .ratio(0.6)
)
.opacity(0.3)
RectangleMark(
    x: .value("Month", $0.month, unit: .month),
    y: .value("Daily Average", $0.dailyAverage),
    width: .ratio(0.6),
    height: 2
)

关键点:

  • BarMarkwidth: .ratio(0.6) 控制柱子宽度为可用空间的 60%
  • RectangleMark 在这里充当”平均值横线”,height: 2 让它看起来像一条线
  • 两种 Mark 共享同一数据,展示不同维度的信息

9:19RuleMark 可以在图表中添加参考线和注释:

RuleMark(
    y: .value("Average", averageValue)
)
.lineStyle(StrokeStyle(lineWidth: 3))
.annotation(position: .top, alignment: .leading) {
    Text("Average: \(averageValue, format: .number)")
        .font(.headline)
        .foregroundStyle(.blue)
}

关键点:

  • RuleMark 在 y 轴指定值处画一条水平参考线
  • .lineStyle(StrokeStyle(lineWidth: 3)) 设置线宽
  • .annotation(position: .top, alignment: .leading) 在参考线顶部添加文本注释

自定义坐标轴样式

13:54)通过 chart modifiers 控制图表比例和颜色映射:

Chart {
    // ... LineMark 定义 ...
}
.chartYScale(domain: 0 ... 200)
.chartForegroundStyleScale([
    "San Francisco": .orange,
    "Cupertino": .pink
])

关键点:

  • .chartYScale(domain:) 手动设置 y 轴范围,避免自动缩放导致不同图表间难以比较
  • .chartForegroundStyleScale([:]) 用字典精确控制每个系列的映射颜色

15:16)自定义 X 轴刻度和标签格式:

.chartXAxis {
    AxisMarks(values: .stride(by: .month)) { value in
        AxisGridLine()
        AxisTick()
        AxisValueLabel(
            format: .dateTime.month(.narrow)
        )
    }
}

关键点:

  • AxisMarks(values: .stride(by: .month)) 按月份步进生成刻度
  • AxisGridLine() 绘制网格线
  • AxisTick() 绘制刻度线
  • AxisValueLabel(format:).dateTime.month(.narrow) 格式化标签为缩写月份(J、A、S 等)

16:17)在 AxisMarks 闭包中做条件判断,实现季度标记:

.chartXAxis {
    AxisMarks(values: .stride(by: .month)) { value in
        if value.as(Date.self)!.isFirstMonthOfQuarter {
            AxisGridLine().foregroundStyle(.black)
            AxisTick().foregroundStyle(.black)
            AxisValueLabel(
                format: .dateTime.month(.narrow)
            )
        } else {
            AxisGridLine()
        }
    }
}

关键点:

  • value.as(Date.self) 将轴值转换为 Date 类型
  • 通过自定义的 isFirstMonthOfQuarter 扩展判断是否为季度首月
  • 季度首月显示完整标记(网格线、刻度线、标签),其他月份只显示网格线
  • 这个模式适用于任何”间隔高亮”的需求

17:00)自定义 Y 轴:

.chartYAxis {
    AxisMarks(preset: .extended, position: .leading)
}

关键点:

  • preset: .extended 扩展轴标记范围
  • position: .leading 将 Y 轴放在左侧

自定义绘图区域样式

18:26)使用 .chartPlotStyle 修改绘图区域的外观:

.chartPlotStyle { plotArea in
    plotArea.frame(height: 60 * 6)
            .background(.pink.opacity(0.2))
            .border(.pink, width: 1)
}

关键点:

  • plotArea 是绘图区域视图的代理,可以直接对其应用 SwiftUI modifier
  • .frame(height: 60 * 6) 手动设定高度,每行 60pt 对应 6 条数据
  • 可以设置背景色、边框等任意 SwiftUI 样式

拖拽选择交互

20:03)通过 .chartOverlay 添加手势交互,实现刷选(brushing)功能:

struct InteractiveBrushingChart: View {
    @State var range: (Date, Date)? = nil

    var body: some View {
        Chart {
            ForEach(data, id: \.day) {
                LineMark(
                    x: .value("Month", $0.day, unit: .day),
                    y: .value("Sales", $0.sales)
                )
                .interpolationMethod(.catmullRom)
                .symbol(Circle().strokeBorder(lineWidth: 2))
            }
            if let (start, end) = range {
                RectangleMark(
                    xStart: .value("Selection Start", start),
                    xEnd: .value("Selection End", end)
                )
                .foregroundStyle(.gray.opacity(0.2))
            }
        }
        .chartOverlay { proxy in
            GeometryReader { nthGeoItem in
                Rectangle().fill(.clear).contentShape(Rectangle())
                    .gesture(DragGesture()
                        .onChanged { value in
                            let xStart = value.startLocation.x - nthGeoItem[proxy.plotAreaFrame].origin.x
                            let xCurrent = value.location.x - nthGeoItem[proxy.plotAreaFrame].origin.x
                            if let dateStart: Date = proxy.value(atX: xStart),
                               let dateCurrent: Date = proxy.value(atX: xCurrent) {
                                range = (dateStart, dateCurrent)
                            }
                        }
                        .onEnded { _ in range = nil }
                    )
            }
        }
    }
}

关键点:

  • .chartOverlay 在图表上方叠加 SwiftUI 视图,支持手势识别
  • proxy.plotAreaFrame 获取绘图区域的坐标框架,用于坐标转换
  • proxy.value(atX:) 将屏幕 x 坐标转换回图表数据域中的 Date 值
  • GeometryReader 用于获取 overlay 视图在屏幕坐标系中的位置
  • RectangleMarkxStartxEnd 渲染选中区域的高亮
  • 手势结束时 range = nil 清除选择状态
  • .contentShape(Rectangle()) 确保透明区域也能响应手势

核心启发

数据看板 App 的”区间高亮”功能

  • 利用 AreaMark + LineMark 叠加展示每日波动范围与平均值趋势,再通过 RuleMark 添加行业基准线
  • .chartOverlay + DragGesture 实现拖拽选择时间区间,用 RectangleMark 渲染选中高亮
  • proxy.value(atX:) 获取选中的日期范围后,联动下方的统计卡片展示区间汇总数据

运动数据可视化

  • BarMark + .position(by:) 对比每周不同运动的时长,横向柱状图展示运动排名
  • .chartYScale(domain:) 固定 y 轴范围,避免不同周之间的视觉误导
  • .chartXAxis 自定义刻度,只标注每周的第一天

财务趋势图

  • LineMark + .interpolationMethod(.catmullRom) 绘制平滑的资产走势
  • AreaMarkyStart/yEnd 展示每月收入/支出范围
  • .chartForegroundStyleScale 自定义支出/收入的配色方案
  • RuleMark 标注预算线,超出预算时改变注释颜色

天气数据对比

  • LineMark 配合 .foregroundStyle(by:) 展示多个城市的温度曲线
  • .symbol(by:) 为每个城市分配不同形状的数据点标记
  • .chartPlotStyle 给绘图区域添加淡蓝色背景,增强”天气”的视觉感知

排行榜动画

  • 利用 SwiftUI 的数据驱动特性,在数据变化时配合 .animation() 实现排行榜位次变化的平滑过渡
  • 用横向 BarMark + .foregroundStyle 渐变展示排名,排名越高颜色越深
  • .chartPlotStyle 设置固定高度,避免数据量变化时图表高度跳动

关联 Session

  • Hello Swift Charts — say hello to swift charts — a flexible framework that helps you create charts entirely in swiftui that look and feel right at home on all apple platforms
  • Design an effective chart — learn how to design focused, approachable, and accessible charts with clear marks, axes, descriptions, interaction, and color
  • What’s new in SwiftUI — join us as we share the latest updates and a glimpse into the future of ui framework design including swift charts

评论

GitHub Issues · utterances