sensorkit

sensorkit

热门

在经批准的研究项目中,使用 SensorKit 获取研究级的传感器数据。当 App 需要配置 SensorKit Entitlement 特权、申请“研究传感器与使用数据”授权,或者获取环境光、运动记录、设备使用情况、键盘指标、访问地点、语音、面部、手腕温度、心电图 (ECG)、光电容积脉搏波 (PPG)、声学设置或睡眠分析数据时使用。对于常规运动数据请路由至 CoreMotion,健康记录与体能训练数据请路由至 HealthKit。

967Star
49Fork
更新于 2026/7/31
SKILL.md
只读
名称
sensorkit
描述

在经批准的研究项目中,使用 SensorKit 获取研究级的传感器数据。当 App 需要配置 SensorKit Entitlement 特权、申请“研究传感器与使用数据”授权,或者获取环境光、运动记录、设备使用情况、键盘指标、访问地点、语音、面部、手腕温度、心电图 (ECG)、光电容积脉搏波 (PPG)、声学设置或睡眠分析数据时使用。对于常规运动数据请路由至 CoreMotion,健康记录与体能训练数据请路由至 HealthKit。

SensorKit

精准选择具体的 SRSensor 并核验其各自的可用性。常规运动/活动功能请使用 CoreMotion,健康记录与体能训练请使用 HealthKit。

目录

概述与要求

SensorKit 允许研究类 App 记录并拉取 iPhone 和 Apple Watch 上的传感器数据。该框架要求:

  1. 获得 Apple 批准的研究项目 —— 在 researchandcare.org 提交研究方案申请。
  2. SensorKit entitlement —— Apple 仅针对获得批准的研究项目签发 com.apple.developer.sensorkit.reader.allow 权限。
  3. 手动配置文件 (Provisioning Profile) —— Xcode 要求使用显式指定且已启用 SensorKit Capability 的 App ID。
  4. 用户授权 —— 系统会弹出“研究传感器与使用数据”授权弹窗,由用户针对每个传感器单独批准。
  5. 延迟获取机制 —— 结合规范的数据暂存期 (Data Holding Period)来设计数据拉取时机。

App 最多可以获取活跃传感器在过去 7 天内记录的历史数据。

Entitlements

将 SensorKit reader entitlement 添加到 .entitlements 文件中。仅列出 Apple 为该研究项目批准的传感器。常见的 entitlement 字段示例如下:

<key>com.apple.developer.sensorkit.reader.allow</key>
<array>
    <string>ambient-light-sensor</string>
    <string>motion-accelerometer</string>
    <string>device-usage</string>
    <string>keyboard-metrics</string>
</array>

在为每个受批准传感器挑选精准的 entitlement 字符串和 NSSensorKitUsageDetail key 时,请查阅Entitlement 与 Usage-Detail 目录。对于特殊传感器,请对照其各自的 SRSensor 文档进行二次核对。

对于手动签名 (Manual Signing),请将 Code Signing Entitlements 设置为该 entitlements 文件,Code Signing Identity 设置为 Apple Developer,Code Signing Style 设置为 Manual,并将 Provisioning Profile 设为带有 SensorKit 功能的显式配置文件。

Info.plist 配置

必须配置以下三个 key:

<!-- 授权弹窗中显示的研究目的描述 -->
<key>NSSensorKitUsageDescription</key>
<string>This study monitors activity patterns for sleep research.</string>

<!-- 链接至您研究项目的隐私政策 -->
<key>NSSensorKitPrivacyPolicyURL</key>
<string>https://example.com/privacy-policy</string>

<!-- 按传感器划分的具体使用说明 -->
<key>NSSensorKitUsageDetail</key>
<dict>
    <key>SRSensorUsageMotion</key>
    <dict>
        <key>Description</key>
        <string>Measures physical activity levels during the study.</string>
        <key>Required</key>
        <true/>
    </dict>
    <key>SRSensorUsageAmbientLightSensor</key>
    <dict>
        <key>Description</key>
        <string>Records ambient light to assess sleep environment.</string>
    </dict>
</dict>

若某传感器的 Requiredtrue 且用户拒绝了该传感器授权,系统会提示用户该研究需要此数据,并提供重新考虑的机会。

请为每个申请的传感器配置准确的 usage-detail 字典。如果映射除上述运动与环境光之外的传感器,请查阅 Entitlement 与 Usage-Detail 目录

用户授权

为您的研究项目所需的传感器申请授权。首次请求时,系统会展示“研究传感器与使用数据”授权弹窗。

import SensorKit

let reader = SRSensorReader(sensor: .ambientLightSensor)

// 一次性申请多个传感器的授权
SRSensorReader.requestAuthorization(
    sensors: [.ambientLightSensor, .accelerometer, .keyboardMetrics]
) { error in
    if let error {
        print("Authorization request failed: \(error)")
    }
}

使用统一的状态处理函数来兼顾首次检查与代理回调变更:

private func applyAuthorizationStatus(
    _ status: SRAuthorizationStatus,
    to reader: SRSensorReader
) {
    switch status {
    case .authorized:
        reader.startRecording()
    case .denied:
        reader.stopRecording()
        // 引导用户前往:设置 > 隐私与安全性 > 研究传感器与使用数据。
    case .notDetermined:
        break // 需要先申请授权。
    @unknown default:
        break
    }
}

applyAuthorizationStatus(reader.authorizationStatus, to: reader)

func sensorReader(_ reader: SRSensorReader, didChange authorizationStatus: SRAuthorizationStatus) {
    applyAuthorizationStatus(authorizationStatus, to: reader)
}

可用传感器

查阅传感器目录,将每个 SRSensor 映射到对应的 Sample 类型。仅申请获得研究项目批准的传感器,并核查所选传感器的可用性与 usage-detail key。

SRSensorReader

SRSensorReader 是获取传感器数据的核心类。每个实例负责读取单个传感器的数据。

import SensorKit

// 为单个传感器创建 reader
let lightReader = SRSensorReader(sensor: .ambientLightSensor)
let keyboardReader = SRSensorReader(sensor: .keyboardMetrics)

// 设置代理以接收回调
lightReader.delegate = self
keyboardReader.delegate = self

Reader 通过 SRSensorReaderDelegate 进行通信。在编写完整的授权、记录、设备拉取和样本拉取生命周期时,请查阅代理方法目录

数据记录与拉取

开始与停止记录

// 开始记录 —— 只要有任意 App 维持订阅,传感器就会保持开启
reader.startRecording()

// 停止记录 —— 当没有任何 App 或系统进程使用时,框架会自动停用传感器
reader.stopRecording()

拉取数据

构建带时间范围和目标设备的 SRFetchRequest,然后将其传递给 reader:

let request = SRFetchRequest()
request.device = SRDevice.current
request.from = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400 * 2)  // 2 天前
request.to = SRAbsoluteTime.current()

reader.fetch(request)

通过代理接收数据结果:

func sensorReader(
    _ reader: SRSensorReader,
    fetching request: SRFetchRequest,
    didFetchResult result: SRFetchResult<AnyObject>
) -> Bool {
    let timestamp = result.timestamp

    switch reader.sensor {
    case .ambientLightSensor:
        if let sample = result.sample as? SRAmbientLightSample {
            let lux = sample.lux
            let chromaticity = sample.chromaticity
            let placement = sample.placement
            processSample(lux: lux, chromaticity: chromaticity, at: timestamp)
        }
    case .keyboardMetrics:
        if let sample = result.sample as? SRKeyboardMetrics {
            let words = sample.totalWords
            let speed = sample.typingSpeed
            processKeyboard(words: words, speed: speed, at: timestamp)
        }
    case .deviceUsageReport:
        if let sample = result.sample as? SRDeviceUsageReport {
            let wakes = sample.totalScreenWakes
            let unlocks = sample.totalUnlocks
            processUsage(wakes: wakes, unlocks: unlocks, at: timestamp)
        }
    default:
        break
    }

    return true  // 返回 true 以继续接收后续结果
}

func sensorReader(_ reader: SRSensorReader, didCompleteFetch request: SRFetchRequest) {
    print("Fetch complete for \(reader.sensor)")
}

func sensorReader(
    _ reader: SRSensorReader,
    fetching request: SRFetchRequest,
    failedWithError error: any Error
) {
    print("Fetch failed: \(error)")
}

result.sample 类型转换 (Cast) 为对应 reader 传感器的 sample 数据结构。部分数据流每个结果返回一个对象,而运动记录、ECG、PPG 和环境气压数据流则可能返回已记录 sample 的数组。

数据暂存期 (Data Holding Period)

SensorKit 对新记录的数据强制实行 24 小时暂存期。如果拉取请求的时间范围与此暂存期重叠,将不会返回任何结果。请围绕该延迟机制来设计数据采集工作流。

SRDevice

SRDevice 用于识别传感器样本的硬件来源。使用它可以区分数据来自 iPhone 还是 Apple Watch。

// 获取当前设备
let currentDevice = SRDevice.current
print("Model: \(currentDevice.model)")
print("System: \(currentDevice.systemName) \(currentDevice.systemVersion)")

// 获取某个传感器对应的所有可用设备
reader.fetchDevices()

通过代理处理拉取到的设备列表:

func sensorReader(_ reader: SRSensorReader, didFetch devices: [SRDevice]) {
    for device in devices {
        let request = SRFetchRequest()
        request.device = device
        request.from = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400)
        request.to = SRAbsoluteTime.current()
        reader.fetch(request)
    }
}

func sensorReader(_ reader: SRSensorReader, fetchDevicesDidFailWithError error: any Error) {
    print("Failed to fetch devices: \(error)")
}

SRDevice 属性说明

属性 类型 描述
model String 用户自定义设备名称
name String 框架定义的设备名称
systemName String 操作系统名称 (iOS, watchOS)
systemVersion String 操作系统版本
productType String 硬件型号标识符
current SRDevice 当前运行设备的类属性

常见错误

切勿:在未配置 Entitlement 的情况下尝试使用 SensorKit

在构建生产环境 reader 之前,务必先取得 Apple 的研究批准、传感器专属的 entitlement 值以及匹配的手动配置文件 (Provisioning Profile)。

切勿:期望即时获取数据

注意遵守数据暂存期;拉取落在暂存期内的数据未返回结果并不代表记录失败。

切勿:在拉取数据前忘记设置代理 (Delegate)

请在调用 startRecording()fetch(_:) 前完成代理赋值;数据结果与失败通知均通过代理回调接收。

切勿:遗漏各个传感器的 Info.plist 使用说明 (Usage Detail)

务必为每个申请的传感器在 Info.plist 配置 中添加精准对应的 usage-detail 条目。

切勿:忽略 SRError 错误码

至少需要对 .invalidEntitlement.noAuthorization.dataInaccessible.fetchRequestInvalid.promptDeclined 以及未来可能新增的未知错误码进行区分处理。有关完整的 switch 逻辑和回调绑定,请查阅完整 Delegate 实现

审核清单

  • [ ] 开发前已获得 Apple 批准的研究项目
  • [ ] com.apple.developer.sensorkit.reader.allow entitlement 仅列出了所需的传感器
  • [ ] 已配置具有显式 App ID 和 SensorKit Capability 的手动 Provisioning Profile
  • [ ] Info.plist 中的 NSSensorKitUsageDescription 明确阐明了研究目的
  • [ ] Info.plist 中的 NSSensorKitPrivacyPolicyURL 提供了有效的隐私政策 URL
  • [ ] 已为每个申请的传感器配置了 NSSensorKitUsageDetail 条目
  • [ ] 已针对核心传感器与可选传感器合理设置 Required 键值
  • [ ] 开始记录前已申请授权,拉取数据前已核查授权状态
  • [ ] 调用 startRecording()fetch(_:) 前已设置代理 (Delegate)
  • [ ] 拉取请求的时间范围已充分考虑 24 小时暂存期

<!-- truncated for translation batch; full body continues in source -->