WWDC Quick Look 💓 By SwiftGGTeam
Get started with HealthKit in visionOS

Get started with HealthKit in visionOS

观看原视频

Highlight

HealthKit 在 visionOS 2 上完整可用,iPad 应用无需改动即可运行,原生应用则可通过空间计算实现图表自适应、多窗口对比和沉浸式健康体验。


核心内容

你的 iPad 健康应用想在 Apple Vision Pro 上跑起来,但不知道 HealthKit 支持到什么程度、需要改多少代码。visionOS 2 给出了答案:HealthKit 的读写、统计、通知能力与 iPadOS 基本一致,数据通过 iCloud 跨设备同步,授权配置也通用。两种应用形态都能用——基于 iOS 17 编译的 iPad 应用自动获得 Designed for iPad 体验,零改动;使用 visionOS SDK 的原生应用则可以充分利用空间画布。

Session 用一个情绪记录应用做案例,演示了从 iPad 适配到原生 visionOS 的三步走:第一步,让图表随窗口缩放自适应,用 onGeometryChange 动态调整数据点数量;第二步,通过 openWindow 支持多窗口并排对比;第三步,用 Full Immersive Space 做沉浸式情绪记录,保存后自动退出沉浸态回到工作空间。Guest User 场景下有特殊限制——已授权应用可以继续读取数据,但不能发起新授权,也不能写入数据,HealthKit 会返回一个新增的错误类型 errorNotPermissibleForGuestUserMode


详细内容

HealthKit 在 visionOS 上的能力

HealthKit 在 visionOS 上的行为与 iPadOS 一致:读写健康数据、聚合统计、注册前后台数据变更通知全部支持(01:52)。用户在 Settings 中管理每个应用的授权,数据通过 iCloud 在设备间同步(02:04)。两种应用形态都可以使用 HealthKit:使用 visionOS SDK 的原生应用,以及基于 iOS 17 编译的 iPad 应用(02:16)。

基础用法跟其他平台一样。先检查可用性:

import HealthKit

if HKHealthStore.isHealthDataAvailable() {
    // Configure HealthKit-powered experiences
} else {
    // Omit HealthKit experiences
}

关键点:

  • isHealthDataAvailable() 是所有 HealthKit 逻辑的入口,在 visionOS 上同样适用
  • 不同设备和 OS 版本可用性可能不同,必须检查
  • 如果要兼容早期 visionOS 版本,这个检查尤其重要(02:43

然后请求授权:

import HealthKitUI
import SwiftUI

func healthDataAccessRequest(
    store: HKHealthStore,
    shareTypes: Set<HKSampleType>,
    readTypes: Set<HKObjectType>? = nil,
    trigger: some Equatable,
    completion: @escaping (Result<Bool, any Error>) -> Void
) -> some View

关键点:

  • healthDataAccessRequest 是 SwiftUI 视图修饰符,触发系统授权弹窗
  • shareTypes 是你想写入的数据类型,readTypes 是你想读取的类型
  • trigger 控制何时弹出授权,只在应用真正需要数据时才触发
  • 只请求你需要的类型,只在需要的时候请求(03:03

图表自适应与多窗口

空间画布的核心优势是窗口可以自由缩放。用 onGeometryChange 让图表数据点随窗口大小动态变化:

import SwiftUI
import HealthKit
import Charts

struct ChartView: View {
    @State var chartBinCount: Int

    var body: some View {
        Chart { ...
            // Chart body
        }
        .onGeometryChange(for: Int.self) { proxy in
            Int(proxy.size.width / 80) // 80 points per chart point
        } action: { newValue in
            chartBinCount = newValue
        }
    }
}

关键点:

  • onGeometryChange 是 SwiftUI 新增 API,监听视图尺寸变化
  • 闭包 proxy -> Int 根据宽度计算数据点数量,这里每 80pt 一个数据点
  • action 闭包拿到新值后更新 @State,驱动图表重绘
  • 窗口拉大时数据点变多,缩小时变少,用户始终看到合理密度的图表(05:59

多窗口对比只需一个按钮:

struct NewChartViewerButton: View {
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button("Open In New Window", systemImage: "plus.rectangle.on.rectangle") {
            openWindow(id: "chart-viewer-window")
        }
    }
}

关键点:

  • openWindow 环境动作通过窗口标识符打开新窗口
  • iPad 上已支持 Split View 的应用,迁移到多窗口几乎没有额外工作
  • 两个图表窗口并排放在空间画布上,比 Split View 更自由(06:33

Guest User 场景的处理

Guest User 是 visionOS 的特性,允许其他人试用设备。HealthKit 在这个场景下有三条限制:已授权应用可以继续读取数据;不能发起新的授权请求,系统会返回错误;不能写入数据,防止访客数据混入机主健康库(08:13)。

写入失败时 HealthKit 返回新增的错误类型:

let sample = HKStateOfMind(date: date, kind: .momentaryEmotion, valence: valence,
                           labels: [label], associations: [association])
do {
    try await healthStore.save(sample)
} catch {
    switch error {
    case HKError.errorNotPermissibleForGuestUserMode:
        // Drop data generated in a Guest User session
    default:
        // Existing error handling
    }
}

关键点:

  • HKError.errorNotPermissibleForGuestUserMode 是 visionOS 新增的错误类型
  • 收到此错误时应当丢弃 Guest 生成的数据,避免后续意外写入机主的健康库
  • 原有的错误处理逻辑放在 default 分支,不影响(09:00

授权请求在 Guest 会话中也会失败,需要在 healthDataAccessRequest 的回调中处理:

.healthDataAccessRequest(store: healthStore,
                         shareTypes: [.stateOfMindType()],
                         readTypes: [.stateOfMindType()],
                         trigger: toggleHealthDataAuthorization) { result in
    switch result {
    case .success: healthDataAuthorized = true
    case .failure(let error as HKError):
        switch (error.code) {
        case .errorNotPermissibleForGuestUserMode:
            // Defer requests for a later time
        default:
            // Existing error handling
        }
    }
}

关键点:

  • Guest 会话中系统不会弹出授权弹窗,调用直接失败
  • Apple 建议将授权请求延迟到非 Guest 会话再发起
  • 读数据不受影响——Guest 继承机主已授予的权限(09:26

最后给用户一个提示:

struct EventView: View {
    @State private var showAlert: Bool = false
    @State private var saveDetails: EmojiType.SaveDetails? = nil

    var body: some View {
        EmojiPicker()
            .alert("Unable to Save Health Data",
                   isPresented: $showAlert,
                   presenting: saveDetails,
                   actions: { _ in },
                   message: { details in
                Text(details.errorString)
            })
    }
}

关键点:

  • Guest 写入失败后弹出 alert 告知数据未保存
  • 这样 Guest 可以体验应用功能,但不会污染机主数据
  • alert 用 presenting 参数传入错误详情,动态显示错误信息(09:58

核心启发

1. 图表类健康数据展示用 onGeometryChange 做响应式密度

  • 为什么值得做:空间画布上窗口大小由用户决定,固定数据点数量的图表要么太稀疏要么太密集,体验差。
  • 怎么开始:给 Chart 视图加 .onGeometryChange,按 proxy.size.width / 常数 算数据点数量,更新 @State 驱动重绘。

2. 对比场景开放多窗口,用 openWindow(id:) 一行搞定

  • 为什么值得做:健康数据经常需要对比(本周 vs 上周、心率 vs 步数),两个独立窗口比一个窗口里的 Tab 切换直观得多。
  • 怎么开始:在已有 Split View 支持的 iPad 应用上加 @Environment(\.openWindow),用窗口标识符打开新窗口,参考 Apple 示例项目 “Bringing multiple windows to your SwiftUI app”。

3. 所有 HealthKit 写入路径都必须处理 errorNotPermissibleForGuestUserMode

  • 为什么值得做:Guest User 写入会静默失败并返回此错误,不处理就可能导致数据丢失或意外写入机主健康库。
  • 怎么开始:在 healthStore.save() 的 catch 分支和 healthDataAccessRequest 的 failure 回调中各加一个 case .errorNotPermissibleForGuestUserMode,丢弃数据并弹出 alert 提示用户。

关联 Session

评论

GitHub Issues · utterances