Build a research and care app, part 3: Visualize progress
Health & Fitness 进阶 14m

构建研究与护理 App(三):可视化进度

Build a research and care app, part 3: Visualize progress

2021年6月9日

在 Apple 官方观看视频

一句话判断

健康数据的可视化不只是画图表,关键在于把「你看你走了多少步」变成「你看你的努力有了什么变化」——这个 Session 教你怎么用 SwiftUI 做到这一点。

这场 Session 讲了什么

这是「Build a research and care app」系列的第三部分,聚焦在如何用 SwiftUI 将 HealthKit 数据以有意义的方式呈现给用户。前两部分讲了数据采集和存储,这一部分解决的是「最后一公里」的问题:让用户看懂自己的健康数据。

Session 以一个示例 App 为线索,展示了从 HealthKit 读取步数、心率等数据后,如何用 SwiftUI 的图表组件(ChartView)和自定义 Shape 来绘制趋势图。重点不是教你画折线图的技术细节,而是如何选择正确的可视化方式:时间跨度用折线图、分类对比用柱状图、目标达成用环形进度。Session 还讲了如何处理数据稀疏的情况——用户可能连续几天没有数据,你的可视化不能在空数据处画一条直线假装没事发生。

另外,Session 讨论了健康数据的隐私边界:呈现数据时要尊重用户的敏感信息,比如不要在小组件(Widget)上显示具体的心率数值,而是用「在目标范围内」这样的模糊表述。

值得深挖的点

SwiftUI Charts 的数据聚合策略。 当你从 HealthKit 读取大量数据点时(比如一年的每日步数),直接全部绘制会导致图表过于密集无法阅读。Session 展示了一个分层聚合的方法:在日视图显示每小时数据,周视图显示每日数据,月视图显示每周数据。通过 Calendar.current.dateComponents 做日期分组,然后用 Dictionary(grouping:) 聚合,最后取每组的平均值或总和。这种「根据时间粒度自动聚合」的模式在健康类 App 中几乎无处不在。

数据缺失的处理哲学。 健康数据天然会有空缺——用户忘了戴手表、传感器断连、或者纯粹是某天没运动。Session 的建议是:在图表中用虚线或透明色段明确标识缺失区间,而不是插值或跳过。这不仅是视觉设计问题,更关乎用户信任——如果你在空缺处画了插值线,用户会质疑其他数据的真实性。HealthKit 的 statisticsCollection API 返回的结果包含了数据区间信息,你可以据此判断哪些日期确实没有数据。

代码片段

import HealthKit

// 从 HealthKit 查询步数统计数据
func fetchStepData(for date: Date) async throws -> [Date: Double] {
    let healthStore = HKHealthStore()
    let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
    let predicate = HKQuery.predicateForSamples(
        withStart: date.startOfMonth(),
        end: date.endOfMonth(),
        options: .strictStartDate
    )

    var results: [Date: Double] = []
    let query = HKStatisticsCollectionQuery(
        quantityType: stepType,
        quantitySamplePredicate: predicate,
        options: .cumulativeSum,
        anchorDate: date.startOfMonth(),
        intervalComponents: DateComponents(day: 1)
    )

    // 每日聚合数据
    query.initialResultsHandler = { _, collection, error in
        collection?.enumerateStatistics(from: date.startOfMonth(),
                                         to: date.endOfMonth()) { stats, _ in
            if let sum = stats.sumQuantity() {
                results[stats.startDate] = sum.doubleValue(for: .count())
            }
        }
    }
    return results
}
// SwiftUI 绘制简单的步数趋势图
struct StepChartView: View {
    let data: [(date: Date, steps: Double)]

    var body: some View {
        VStack {
            // 用 Path 绘制折线
            GeometryReader { geo in
                Path { path in
                    let maxSteps = data.map(\.steps).max() ?? 1
                    for (index, point) in data.enumerated() {
                        let x = geo.size.width * CGFloat(index) / CGFloat(data.count)
                        let y = geo.size.height * (1 - point.steps / maxSteps)
                        if index == 0 {
                            path.move(to: CGPoint(x: x, y: y))
                        } else {
                            path.addLine(to: CGPoint(x: x, y: y))
                        }
                    }
                }
                .stroke(Color.blue, lineWidth: 2)
            }
            .frame(height: 200)
        }
    }
}
// 环形进度视图——展示目标完成度
struct RingProgressView: View {
    let progress: Double // 0.0 到 1.0

    var body: some View {
        ZStack {
            // 背景环
            Circle()
                .stroke(Color.gray.opacity(0.2), lineWidth: 12)
            // 进度环
            Circle()
                .trim(from: 0, to: progress)
                .stroke(Color.green, style: StrokeStyle(lineWidth: 12, lineCap: .round))
                .rotationEffect(.degrees(-90))
            // 中间显示百分比
            Text("\(Int(progress * 100))%")
                .font(.title2.bold())
        }
        .frame(width: 100, height: 100)
    }
}

最佳实践

  • 健康数据的可视化应该突出「趋势」而不是「绝对值」——用户更关心「我在变好还是变差」而不是「今天走了 8432 步」
  • 在 Widget 上展示健康数据时使用模糊化表述(「在目标范围内」「比昨天多了」),避免暴露敏感的具体数值
  • 数据加载期间显示骨架屏或上次的缓存数据,不要显示空的图表区域——空白会让用户以为 App 坏了
  • 给用户提供时间范围选择器(7 天 / 30 天 / 90 天 / 1 年),不同时间粒度下的数据聚合逻辑要一致

还有什么值得关注

  • SwiftUI Charts 框架在后续 WWDC 版本中有大幅增强,iOS 16 引入了原生 Chart 组件
  • HealthKit 的 HKStatisticsCollectionQuery 支持设置增量更新 handler,可以实时响应新数据写入
  • 考虑为色盲用户提供图案或形状来区分数据系列,不要完全依赖颜色
WWDC 2021