
Sensorkit
- 2k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
sensorkit is an agent skill for Access research-grade sensor data using SensorKit for approved studies. Use when an app needs SensorKit entitlement setup, Research Sensor & Usage Data authorization, ambient light
About
Access research-grade sensor data using SensorKit for approved studies. Use when an app needs SensorKit entitlement setup, Research Sensor & Usage Data authorization, ambient light, recorded motion, device usage, keyboard metrics, visits, speech, face, wrist temperature, ECG, PPG, acoustic settings, or sleep-session data. Route ordinary motion to CoreMotion and health records/workouts to HealthKit The sensorkit skill documents workflows and patterns from the repository SKILL.md. --- name: sensorkit description: "Access research-grade sensor data using SensorKit for approved studies. Use when an app needs SensorKit entitlement setup, Research Sensor & Usage Data authorization, ambient light, recorded motion, device usage, keyboard metrics, visits, speech, face, wrist temperature, ECG, PPG, acoustic settings, or sleep-session data. Route ordinary motion to CoreMotion and health records/workouts to HealthKit." --- # SensorKit Collect research-grade sensor data from iOS and watchOS devices for approved research studies. SensorKit provides access to ambient light, motion, device usage, keyboard metrics, visits, phone/messaging usage, speech metrics, face metrics, wrist temperature, hea.
- [Overview and Requirements](#overview-and-requirements)
- [Entitlements](#entitlements)
- [Info.plist Configuration](#infoplist-configuration)
- [Authorization](#authorization)
- [Available Sensors](#available-sensors)
Sensorkit by the numbers
- 2,034 all-time installs (skills.sh)
- +103 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #119 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
sensorkit capabilities & compatibility
- Capabilities
- [overview and requirements](#overview and requir · [entitlements](#entitlements) · [info.plist configuration](#infoplist configurat · [authorization](#authorization) · [available sensors](#available sensors)
- Use cases
- documentation
What sensorkit says it does
--- name: sensorkit description: "Access research-grade sensor data using SensorKit for approved studies.
Route ordinary motion to CoreMotion and health records/workouts to HealthKit." --- # SensorKit Collect research-grade sensor data from iOS and watchOS devices for approved research studies.
SensorKit provides access to ambient light, motion, device usage, keyboard metrics, visits, phone/messaging usage, speech metrics, face metrics, wrist temperature, heart rate, ECG, and PPG data.
This is not a general-purpose sensor API -- use CoreMotion for ordinary accelerometer, gyroscope, pedometer, or activity-recognition features, and HealthKit for health records and workouts.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill sensorkitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
What problem does sensorkit solve for developers using the documented workflows?
Access research-grade sensor data using SensorKit for approved studies. Use when an app needs SensorKit entitlement setup, Research Sensor & Usage Data authorization, ambient light, recorded motion, d
Who is it for?
Developers working with sensorkit patterns described in the skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill documented scope.
When should I use this skill?
Access research-grade sensor data using SensorKit for approved studies. Use when an app needs SensorKit entitlement setup, Research Sensor & Usage Data authorization, ambient light, recorded motion, d
What you get
Grounded guidance and workflows from SKILL.md for sensorkit.
- SensorKit setup checklist
- Entitlement and Info.plist configuration
By the numbers
- Documents 5 SensorKit sample types: ambient light, keyboard metrics, wrist temperature, ECG, and PPG
- Enforces 24-hour data fetch hold constraint per Apple requirements
Files
SensorKit
Collect research-grade sensor data from iOS and watchOS devices for approved research studies. SensorKit provides access to ambient light, motion, device usage, keyboard metrics, visits, phone/messaging usage, speech metrics, face metrics, wrist temperature, heart rate, ECG, and PPG data. Targets Swift 6.3 / iOS 26+.
SensorKit is restricted to Apple-approved research studies. Apps must submit a research proposal to Apple and receive the com.apple.developer.sensorkit.reader.allow entitlement before any sensor data is accessible. This is not a general-purpose sensor API -- use CoreMotion for ordinary accelerometer, gyroscope, pedometer, or activity-recognition features, and HealthKit for health records and workouts.
Contents
- Overview and Requirements
- Entitlements
- Info.plist Configuration
- Authorization
- Available Sensors
- SRSensorReader
- Recording and Fetching Data
- SRDevice
- Common Mistakes
- Review Checklist
- References
Overview and Requirements
SensorKit enables research apps to record and fetch sensor data across iPhone and Apple Watch. The framework requires:
1. Apple-approved research study -- submit a proposal at researchandcare.org. 2. SensorKit entitlement -- Apple grants com.apple.developer.sensorkit.reader.allow only for approved studies. 3. Manual provisioning profile -- Xcode requires an explicit App ID with the SensorKit capability enabled. 4. User authorization -- the system presents a Research Sensor & Usage Data sheet that users approve per-sensor. 5. 24-hour data hold -- newly recorded data is inaccessible for 24 hours, giving users time to delete data they do not want to share.
An app can access up to 7 days of prior recorded data for an active sensor.
Entitlements
Add the SensorKit reader entitlement to a .entitlements file. List only the sensors Apple approved for the study. Common entitlement values include:
<key>com.apple.developer.sensorkit.reader.allow</key>
<array>
<string>ambient-light-sensor</string>
<string>motion-accelerometer</string>
<string>motion-rotation-rate</string>
<string>device-usage</string>
<string>keyboard-metrics</string>
<string>messages-usage</string>
<string>phone-usage</string>
<string>visits</string>
<string>pedometer</string>
<string>on-wrist</string>
<string>speech-metrics-siri</string>
<string>speech-metrics-telephony</string>
<string>ambient-pressure</string>
<string>ecg</string>
<string>ppg</string>
</array>Verify newer or specialized sensors against their individual SRSensor pages. For example, Apple's ECG and PPG sensor pages explicitly require ecg and ppg entitlement values in addition to their NSSensorKitUsageDetail entries.
For manual signing, set Code Signing Entitlements to the entitlements file, Code Signing Identity to Apple Developer, Code Signing Style to Manual, and Provisioning Profile to the explicit profile with SensorKit capability.
Info.plist Configuration
Three keys are required:
<!-- Study purpose shown in the authorization sheet -->
<key>NSSensorKitUsageDescription</key>
<string>This study monitors activity patterns for sleep research.</string>
<!-- Link to your study's privacy policy -->
<key>NSSensorKitPrivacyPolicyURL</key>
<string>https://example.com/privacy-policy</string>
<!-- Per-sensor usage explanations -->
<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>If Required is true and the user denies that sensor, the system warns them that the study needs it and offers a chance to reconsider.
Use the exact usage-detail dictionary for each requested sensor. Examples: motion sensors use SRSensorUsageMotion, ambient pressure uses SRSensorUsageElevation, ECG uses SRSensorUsageECG, PPG uses SRSensorUsagePPG, heart rate uses SRSensorUsageHeartRate, and wrist temperature uses SRSensorUsageWristTemperature.
Authorization
Request authorization for the sensors your study needs. The system shows the Research Sensor & Usage Data sheet on first request.
import SensorKit
let reader = SRSensorReader(sensor: .ambientLightSensor)
// Request authorization for multiple sensors at once
SRSensorReader.requestAuthorization(
sensors: [.ambientLightSensor, .accelerometer, .keyboardMetrics]
) { error in
if let error {
print("Authorization request failed: \(error)")
}
}Check a reader's current status before recording:
switch reader.authorizationStatus {
case .authorized:
reader.startRecording()
case .denied:
// User declined -- direct to Settings > Privacy > Research Sensor & Usage Data
break
case .notDetermined:
// Request authorization first
break
@unknown default:
break
}Monitor status changes through the delegate:
func sensorReader(_ reader: SRSensorReader, didChange authorizationStatus: SRAuthorizationStatus) {
switch authorizationStatus {
case .authorized:
reader.startRecording()
case .denied:
reader.stopRecording()
default:
break
}
}Available Sensors
Device Sensors
| Sensor | Type | Sample Type |
|---|---|---|
.deviceUsageReport | Device usage | SRDeviceUsageReport |
.keyboardMetrics | Keyboard activity | SRKeyboardMetrics |
.onWristState | Watch wrist state | SRWristDetection |
.acousticSettings | Acoustic/accessibility settings | SRAcousticSettings |
App Activity Sensors
| Sensor | Type | Sample Type |
|---|---|---|
.messagesUsageReport | Messages app usage | SRMessagesUsageReport |
.phoneUsageReport | Phone call usage | SRPhoneUsageReport |
User Activity Sensors
| Sensor | Type | Sample Type |
|---|---|---|
.accelerometer | Acceleration data | [CMRecordedAccelerometerData] |
.rotationRate | Rotation rate | [CMRecordedRotationRateData] |
.pedometerData | Step/distance data | CMPedometerData |
.visits | Visited locations | SRVisit |
.mediaEvents | Media interactions | SRMediaEvent |
.faceMetrics | Face expressions | SRFaceMetrics |
.heartRate | Heart rate | CMHighFrequencyHeartRateData |
.odometer | Speed/slope | CMOdometerData |
.siriSpeechMetrics | Siri speech | SRSpeechMetrics |
.telephonySpeechMetrics | Phone speech | SRSpeechMetrics |
.wristTemperature | Wrist temp (sleep) | SRWristTemperatureSession |
.sleepSessions | Sleep session summaries | SRSleepSession |
.photoplethysmogram | PPG stream | [SRPhotoplethysmogramSample] |
.electrocardiogram | ECG stream | [SRElectrocardiogramSample] |
Environment Sensors
| Sensor | Type | Sample Type |
|---|---|---|
.ambientLightSensor | Ambient light | SRAmbientLightSample |
.ambientPressure | Pressure/temp | [CMRecordedPressureData] |
SRSensorReader
SRSensorReader is the central class for accessing sensor data. Each instance reads from a single sensor.
import SensorKit
// Create a reader for one sensor
let lightReader = SRSensorReader(sensor: .ambientLightSensor)
let keyboardReader = SRSensorReader(sensor: .keyboardMetrics)
// Assign delegate to receive callbacks
lightReader.delegate = self
keyboardReader.delegate = selfThe reader communicates entirely through SRSensorReaderDelegate:
| Delegate Method | Purpose |
|---|---|
sensorReader(_:didChange:) | Authorization status changed |
sensorReaderWillStartRecording(_:) | Recording is about to start |
sensorReader(_:startRecordingFailedWithError:) | Recording failed to start |
sensorReaderDidStopRecording(_:) | Recording stopped |
sensorReader(_:stopRecordingFailedWithError:) | Recording failed to stop |
sensorReader(_:didFetch:) | Devices fetched |
sensorReader(_:fetchDevicesDidFailWithError:) | Device fetch failed |
sensorReader(_:fetching:didFetchResult:) | Sample received |
sensorReader(_:didCompleteFetch:) | Fetch completed |
sensorReader(_:fetching:failedWithError:) | Fetch failed |
Recording and Fetching Data
Start and Stop Recording
// Begin recording -- sensor stays active as long as any app has a stake
reader.startRecording()
// Stop recording -- framework deactivates the sensor when
// no app or system process is using it
reader.stopRecording()Fetch Data
Build an SRFetchRequest with a time range and target device, then pass it to the reader:
let request = SRFetchRequest()
request.device = SRDevice.current
request.from = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400 * 2) // 2 days ago
request.to = SRAbsoluteTime.current()
reader.fetch(request)Receive results through the delegate:
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 // Return true to continue receiving results
}
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)")
}Cast result.sample to the sample shape for the reader's sensor. Some streams return one object per result, while recorded motion, ECG, PPG, and ambient pressure streams can return arrays of recorded samples.
Data Holding Period
SensorKit imposes a 24-hour holding period on newly recorded data. Fetch requests whose time range overlaps this period return no results. Design data collection workflows around this delay.
SRDevice
SRDevice identifies the hardware source for sensor samples. Use it to distinguish data from iPhone versus Apple Watch.
// Get the current device
let currentDevice = SRDevice.current
print("Model: \(currentDevice.model)")
print("System: \(currentDevice.systemName) \(currentDevice.systemVersion)")
// Fetch all available devices for a sensor
reader.fetchDevices()Handle fetched devices through the delegate:
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 Properties
| Property | Type | Description |
|---|---|---|
model | String | User-defined device name |
name | String | Framework-defined device name |
systemName | String | OS name (iOS, watchOS) |
systemVersion | String | OS version |
productType | String | Hardware identifier |
current | SRDevice | Class property for the running device |
Common Mistakes
DON'T: Attempt to use SensorKit without the entitlement
// WRONG -- fails at runtime with SRError.invalidEntitlement
let reader = SRSensorReader(sensor: .ambientLightSensor)
reader.startRecording()
// CORRECT -- obtain entitlement from Apple first, configure manual
// provisioning profile, then use SensorKitDON'T: Expect immediate data access
// WRONG -- fetching data recorded moments ago returns nothing
reader.startRecording()
// ... record for a few minutes ...
let request = SRFetchRequest()
request.from = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 300)
request.to = SRAbsoluteTime.current()
reader.fetch(request) // Empty results due to 24-hour hold
// CORRECT -- fetch data that is at least 24 hours old
request.from = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400 * 3)
request.to = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400)
reader.fetch(request)DON'T: Forget to set the delegate before fetching
// WRONG -- no delegate means no callbacks, results are silently lost
let reader = SRSensorReader(sensor: .accelerometer)
reader.startRecording()
reader.fetch(request)
// CORRECT -- assign delegate first
reader.delegate = self
reader.startRecording()
reader.fetch(request)DON'T: Skip per-sensor Info.plist usage detail
// WRONG -- missing NSSensorKitUsageDetail for the sensor
// Authorization sheet shows no explanation, user is less likely to approve
// CORRECT -- add usage detail for every sensor you request
// See Info.plist Configuration section aboveDON'T: Ignore SRError codes
// WRONG -- generic error handling
func sensorReader(_ reader: SRSensorReader, fetching: SRFetchRequest, failedWithError error: any Error) {
print("Error")
}
// CORRECT -- handle specific error codes
func sensorReader(_ reader: SRSensorReader, fetching: SRFetchRequest, failedWithError error: any Error) {
if let srError = error as? SRError {
switch srError.code {
case .invalidEntitlement:
// Entitlement missing or sensor not in entitlement array
break
case .noAuthorization:
// User has not authorized this sensor
break
case .dataInaccessible:
// Data in 24-hour holding period or otherwise unavailable
break
case .fetchRequestInvalid:
// Invalid time range or device
break
case .promptDeclined:
// User declined the authorization prompt
break
@unknown default:
break
}
}
}Review Checklist
- [ ] Apple-approved research study in place before development
- [ ]
com.apple.developer.sensorkit.reader.allowentitlement lists only needed sensors - [ ] Manual provisioning profile with explicit App ID and SensorKit capability
- [ ]
NSSensorKitUsageDescriptionin Info.plist with clear study purpose - [ ]
NSSensorKitPrivacyPolicyURLin Info.plist with valid privacy policy URL - [ ]
NSSensorKitUsageDetailentries for every requested sensor - [ ]
Requiredkey set appropriately for essential vs. optional sensors - [ ] Authorization requested before recording, status checked before fetching
- [ ] Delegate assigned before calling
startRecording()orfetch(_:) - [ ] Fetch request time ranges account for 24-hour data holding period
- [ ]
SRErrorcodes handled in all failure delegate methods - [ ]
fetchDevices()used to discover available devices before fetching - [ ]
stopRecording()called when data collection is complete - [ ]
sensorReader(_:fetching:didFetchResult:)returnstrueto continue orfalseto stop
References
- Extended patterns (delegate wiring, multi-sensor manager, sample type details): references/sensorkit-patterns.md
- SensorKit framework
- SRSensorReader
- SRSensor
- SRDevice
- SRFetchRequest
- Configuring your project for sensor reading
- com.apple.developer.sensorkit.reader.allow
{
"skill_name": "sensorkit",
"evals": [
{
"id": 1,
"prompt": "Draft a setup checklist for an approved sleep research app that uses SensorKit ambient light, keyboard metrics, wrist temperature, ECG, and PPG. Include entitlement values, Info.plist usage-detail keys, authorization flow, and fetch timing constraints.",
"expected_output": "A SensorKit setup checklist grounded in Apple requirements for approved research studies, with correct entitlement and Info.plist details and the 24-hour hold.",
"files": [],
"expectations": [
"States that SensorKit requires an Apple-approved research study and the com.apple.developer.sensorkit.reader.allow entitlement.",
"Includes ECG and PPG entitlement values and their NSSensorKitUsageDetail dictionaries.",
"Mentions NSSensorKitUsageDescription, NSSensorKitPrivacyPolicyURL, and per-sensor NSSensorKitUsageDetail entries.",
"Explains requestAuthorization, per-sensor user approval, and the 24-hour holding period before fetch results are available."
]
},
{
"id": 2,
"prompt": "Review this SensorKit fetch handler design for sample-type mistakes: it casts accelerometer to CMAccelerometerData, rotation rate to CMGyroData, wristTemperature to SRWristTemperature, ECG to SRElectrocardiogramSample, and PPG to SRPhotoplethysmogramSample. Return corrected sample shapes and delegate callbacks that should be implemented.",
"expected_output": "A correction note that identifies the wrong casts and names the required SRSensorReaderDelegate callbacks.",
"files": [],
"expectations": [
"Corrects accelerometer and rotation rate to recorded CoreMotion sample arrays.",
"Corrects wrist temperature to SRWristTemperatureSession, with contained SRWristTemperature readings.",
"Corrects ECG and PPG to arrays of SRElectrocardiogramSample and SRPhotoplethysmogramSample.",
"Includes stopRecordingFailedWithError, fetchDevicesDidFailWithError, didFetchResult, didCompleteFetch, and fetching failed callbacks."
]
},
{
"id": 3,
"prompt": "A fitness app without a research study wants live step counts, workout heart-rate summaries, and an Apple Watch motion interaction. Explain whether SensorKit is the right framework and where each responsibility should be routed.",
"expected_output": "A boundary memo that refuses to use SensorKit for ordinary fitness features and routes work to CoreMotion and HealthKit.",
"files": [],
"expectations": [
"Says SensorKit is not appropriate without an Apple-approved research study and entitlement.",
"Routes live steps and motion interaction work to CoreMotion.",
"Routes workout and heart-rate record handling to HealthKit.",
"Avoids giving SensorKit implementation steps for the non-research fitness app."
]
}
]
}
SensorKit Extended Patterns
Overflow reference for the sensorkit skill. Contains delegate wiring, multi-sensor management, and detailed sample type usage that exceed the main skill file's scope.
Contents
- Full Delegate Implementation
- Multi-Sensor Manager
- Ambient Light Samples
- Keyboard Metrics Deep Dive
- Device Usage Reports
- Phone and Messages Usage
- Visit Tracking
- Media Events
- Wrist Detection
- Speech Metrics
- Face Metrics
- Wrist Temperature
- Electrocardiogram and PPG
- SRAbsoluteTime Utilities
- Deletion Records
- Testing Considerations
Full Delegate Implementation
A complete SRSensorReaderDelegate implementation covering all callbacks:
import SensorKit
final class SensorReaderHandler: NSObject, SRSensorReaderDelegate {
// MARK: - Authorization
func sensorReader(_ reader: SRSensorReader, didChange authorizationStatus: SRAuthorizationStatus) {
switch authorizationStatus {
case .authorized:
reader.startRecording()
case .denied:
handleDenied(sensor: reader.sensor)
case .notDetermined:
break
@unknown default:
break
}
}
// MARK: - Recording
func sensorReaderWillStartRecording(_ reader: SRSensorReader) {
print("Recording will start for \(reader.sensor)")
}
func sensorReader(_ reader: SRSensorReader, startRecordingFailedWithError error: any Error) {
print("Recording failed for \(reader.sensor): \(error)")
}
func sensorReaderDidStopRecording(_ reader: SRSensorReader) {
print("Recording stopped for \(reader.sensor)")
}
func sensorReader(_ reader: SRSensorReader, stopRecordingFailedWithError error: any Error) {
print("Stop recording failed for \(reader.sensor): \(error)")
}
// MARK: - Device Fetching
func sensorReader(_ reader: SRSensorReader, didFetch devices: [SRDevice]) {
for device in devices {
fetchData(for: reader, from: device)
}
}
func sensorReader(_ reader: SRSensorReader, fetchDevicesDidFailWithError error: any Error) {
print("Device fetch failed: \(error)")
}
// MARK: - Data Fetching
func sensorReader(
_ reader: SRSensorReader,
fetching request: SRFetchRequest,
didFetchResult result: SRFetchResult<AnyObject>
) -> Bool {
processSample(result, for: reader.sensor)
return true // true = continue fetching, false = stop
}
func sensorReader(_ reader: SRSensorReader, didCompleteFetch request: SRFetchRequest) {
print("Fetch complete for \(reader.sensor)")
}
func sensorReader(
_ reader: SRSensorReader,
fetching request: SRFetchRequest,
failedWithError error: any Error
) {
handleFetchError(error, sensor: reader.sensor)
}
// MARK: - Private
private func fetchData(for reader: SRSensorReader, from device: SRDevice) {
let request = SRFetchRequest()
request.device = device
// Fetch data from 3 days ago to 1 day ago (avoids 24-hour hold)
request.from = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400 * 3)
request.to = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400)
reader.fetch(request)
}
private func handleDenied(sensor: SRSensor) {
// Log or notify that the user denied this sensor
}
private func processSample(_ result: SRFetchResult<AnyObject>, for sensor: SRSensor) {
// Route to sensor-specific processing
}
private func handleFetchError(_ error: any Error, sensor: SRSensor) {
if let srError = error as? SRError {
switch srError.code {
case .invalidEntitlement:
print("Missing entitlement for \(sensor)")
case .noAuthorization:
print("No authorization for \(sensor)")
case .dataInaccessible:
print("Data inaccessible for \(sensor) -- may be in holding period")
case .fetchRequestInvalid:
print("Invalid fetch request for \(sensor)")
case .promptDeclined:
print("User declined prompt for \(sensor)")
@unknown default:
print("Unknown error for \(sensor): \(error)")
}
}
}
}Multi-Sensor Manager
Manage multiple sensors through a single coordinator:
import SensorKit
final class SensorKitManager: NSObject, SRSensorReaderDelegate {
private var readers: [SRSensor: SRSensorReader] = [:]
private var collectedSamples: [SRSensor: [Any]] = [:]
private let studySensors: Set<SRSensor> = [
.ambientLightSensor,
.accelerometer,
.keyboardMetrics,
.deviceUsageReport,
.visits
]
// MARK: - Setup
func configure() {
for sensor in studySensors {
let reader = SRSensorReader(sensor: sensor)
reader.delegate = self
readers[sensor] = reader
}
}
func requestAuthorization() {
SRSensorReader.requestAuthorization(sensors: studySensors) { error in
if let error {
print("Authorization failed: \(error)")
}
}
}
// MARK: - Recording
func startAllRecording() {
for (sensor, reader) in readers {
guard reader.authorizationStatus == .authorized else {
print("Skipping \(sensor) -- not authorized")
continue
}
reader.startRecording()
}
}
func stopAllRecording() {
for reader in readers.values {
reader.stopRecording()
}
}
// MARK: - Fetching
func fetchAllData(daysBack: Int = 3) {
for reader in readers.values {
guard reader.authorizationStatus == .authorized else { continue }
reader.fetchDevices()
}
}
// MARK: - SRSensorReaderDelegate
func sensorReader(_ reader: SRSensorReader, didChange authorizationStatus: SRAuthorizationStatus) {
if authorizationStatus == .authorized {
reader.startRecording()
}
}
func sensorReader(_ reader: SRSensorReader, didFetch devices: [SRDevice]) {
for device in devices {
let request = SRFetchRequest()
request.device = device
request.from = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400 * 3)
request.to = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400)
reader.fetch(request)
}
}
func sensorReader(
_ reader: SRSensorReader,
fetching request: SRFetchRequest,
didFetchResult result: SRFetchResult<AnyObject>
) -> Bool {
var samples = collectedSamples[reader.sensor] ?? []
samples.append(result.sample)
collectedSamples[reader.sensor] = samples
return true
}
func sensorReader(_ reader: SRSensorReader, didCompleteFetch request: SRFetchRequest) {
let count = collectedSamples[reader.sensor]?.count ?? 0
print("Fetched \(count) samples for \(reader.sensor)")
}
func sensorReader(
_ reader: SRSensorReader,
fetching request: SRFetchRequest,
failedWithError error: any Error
) {
print("Fetch error for \(reader.sensor): \(error)")
}
func sensorReader(_ reader: SRSensorReader, fetchDevicesDidFailWithError error: any Error) {
print("Device fetch error for \(reader.sensor): \(error)")
}
}Ambient Light Samples
SRAmbientLightSample provides lux, chromaticity, and sensor placement:
func processAmbientLight(_ result: SRFetchResult<AnyObject>) {
guard let sample = result.sample as? SRAmbientLightSample else { return }
// Illuminance in lux
let luxValue = sample.lux.value // Double
let luxUnit = sample.lux.unit // UnitIlluminance
// Chromaticity coordinates (CIE 1931 xy)
let chromX = sample.chromaticity.x // Float32
let chromY = sample.chromaticity.y // Float32
// Sensor placement relative to light source
switch sample.placement {
case .frontTop:
print("Light from above front")
case .frontBottom:
print("Light from below front")
case .frontLeft, .frontRight:
print("Light from side")
case .frontTopLeft, .frontTopRight:
print("Light from upper corner")
case .frontBottomLeft, .frontBottomRight:
print("Light from lower corner")
case .unknown:
print("Unknown placement")
@unknown default:
break
}
print("Ambient light: \(luxValue) lux, chromaticity: (\(chromX), \(chromY))")
}Keyboard Metrics Deep Dive
SRKeyboardMetrics provides extensive typing analytics:
Basic Metrics
func processKeyboardMetrics(_ result: SRFetchResult<AnyObject>) {
guard let metrics = result.sample as? SRKeyboardMetrics else { return }
// Session info
let duration = metrics.duration
let keyboardID = metrics.keyboardIdentifier
let inputModes = metrics.inputModes // Active languages
let sessions = metrics.sessionIdentifiers
// Quantitative metrics
let totalWords = metrics.totalWords
let totalTaps = metrics.totalTaps
let totalDeletes = metrics.totalDeletes
let totalEmojis = metrics.totalEmojis
let totalAutoCorrections = metrics.totalAutoCorrections
let typingSpeed = metrics.typingSpeed // Characters per second
// Keyboard dimensions
let width = metrics.width // Measurement<UnitLength>
let height = metrics.height // Measurement<UnitLength>
print("Session: \(duration)s, \(totalWords) words at \(typingSpeed) chars/sec")
}Correction Metrics
func analyzeCorrections(_ metrics: SRKeyboardMetrics) {
let corrections = [
"Auto": metrics.totalAutoCorrections,
"Space": metrics.totalSpaceCorrections,
"Retro": metrics.totalRetroCorrections,
"Transposition": metrics.totalTranspositionCorrections,
"Insert key": metrics.totalInsertKeyCorrections,
"Skip touch": metrics.totalSkipTouchCorrections,
"Near key": metrics.totalNearKeyCorrections,
"Substitution": metrics.totalSubstitutionCorrections,
"Hit test": metrics.totalHitTestCorrections
]
for (type, count) in corrections where count > 0 {
print("\(type) corrections: \(count)")
}
}Sentiment Analysis
func analyzeSentiment(_ metrics: SRKeyboardMetrics) {
let categories: [SRKeyboardMetrics.SentimentCategory] = [
.positive, .sad, .anger, .anxiety,
.confused, .down, .lowEnergy, .health,
.death, .absolutist
]
for category in categories {
let wordCount = metrics.wordCount(for: category)
let emojiCount = metrics.emojiCount(for: category)
if wordCount > 0 || emojiCount > 0 {
print("\(category): \(wordCount) words, \(emojiCount) emojis")
}
}
}Timing Distributions
Timing metrics use SRKeyboardMetrics.ProbabilityMetric, which contains a distribution of sample values:
func analyzeTimings(_ metrics: SRKeyboardMetrics) {
// Touch down to touch up duration for any key
let touchDuration = metrics.touchDownUp
let samples = touchDuration.distributionSampleValues // [Measurement<UnitDuration>]
if !samples.isEmpty {
let avgMs = samples.map { $0.converted(to: .milliseconds).value }
.reduce(0, +) / Double(samples.count)
print("Average key press: \(avgMs)ms")
}
// QuickType (swipe) typing speed
let pathSpeed = metrics.pathTypingSpeed // Words per minute
print("Swipe speed: \(pathSpeed) WPM")
}Device Usage Reports
SRDeviceUsageReport provides screen time, unlock, and per-app usage data:
func processDeviceUsage(_ result: SRFetchResult<AnyObject>) {
guard let report = result.sample as? SRDeviceUsageReport else { return }
// Summary metrics
let reportDuration = report.duration
let screenWakes = report.totalScreenWakes
let unlocks = report.totalUnlocks
let unlockDuration = report.totalUnlockDuration
print("Wakes: \(screenWakes), Unlocks: \(unlocks), Duration: \(unlockDuration)s")
// Per-category app usage
for (category, apps) in report.applicationUsageByCategory {
print("Category: \(category.rawValue)")
for app in apps {
let bundleID = app.bundleIdentifier ?? "unknown"
let usageTime = app.usageTime
print(" \(bundleID): \(usageTime)s")
// Text input sessions within this app
for session in app.textInputSessions {
let inputDuration = session.duration
let inputType = session.sessionType
switch inputType {
case .keyboard:
print(" Keyboard input: \(inputDuration)s")
case .dictation:
print(" Dictation input: \(inputDuration)s")
case .pencil:
print(" Pencil input: \(inputDuration)s")
case .thirdPartyKeyboard:
print(" Third-party keyboard: \(inputDuration)s")
@unknown default:
break
}
}
}
}
// Notification interactions
for (category, notifications) in report.notificationUsageByCategory {
for notification in notifications {
let event = notification.event
switch event {
case .received:
print("Notification received: \(notification.bundleIdentifier ?? "unknown")")
case .appLaunch:
print("Notification opened app")
case .clear, .hide, .silence:
print("Notification dismissed")
default:
break
}
}
}
}Phone and Messages Usage
Phone Usage
func processPhoneUsage(_ result: SRFetchResult<AnyObject>) {
guard let report = result.sample as? SRPhoneUsageReport else { return }
let duration = report.duration
let incoming = report.totalIncomingCalls
let outgoing = report.totalOutgoingCalls
let callDuration = report.totalPhoneCallDuration
let contacts = report.totalUniqueContacts
print("Calls: \(incoming) in / \(outgoing) out, Duration: \(callDuration)s")
print("Unique contacts: \(contacts)")
}Messages Usage
func processMessagesUsage(_ result: SRFetchResult<AnyObject>) {
guard let report = result.sample as? SRMessagesUsageReport else { return }
let duration = report.duration
let incoming = report.totalIncomingMessages
let outgoing = report.totalOutgoingMessages
let contacts = report.totalUniqueContacts
print("Messages: \(incoming) in / \(outgoing) out over \(duration)s")
print("Unique contacts: \(contacts)")
}Visit Tracking
SRVisit provides categorized location visit data with distance from home:
func processVisit(_ result: SRFetchResult<AnyObject>) {
guard let visit = result.sample as? SRVisit else { return }
let visitID = visit.identifier
let arrival = visit.arrivalDateInterval
let departure = visit.departureDateInterval
let distance = visit.distanceFromHome // CLLocationDistance in meters
switch visit.locationCategory {
case .home:
print("At home")
case .work:
print("At work, \(distance)m from home")
case .school:
print("At school")
case .gym:
print("At gym")
case .unknown:
print("Unknown location, \(distance)m from home")
@unknown default:
break
}
print("Visit \(visitID): arrived \(arrival), departed \(departure)")
}Media Events
SRMediaEvent tracks interactions with images and videos in messaging apps:
func processMediaEvent(_ result: SRFetchResult<AnyObject>) {
guard let event = result.sample as? SRMediaEvent else { return }
let mediaID = event.mediaIdentifier
switch event.eventType {
case .onScreen:
print("Media \(mediaID) appeared on screen")
case .offScreen:
print("Media \(mediaID) went off screen")
@unknown default:
break
}
}Wrist Detection
SRWristDetection reports Apple Watch wrist state and configuration:
func processWristDetection(_ result: SRFetchResult<AnyObject>) {
guard let wrist = result.sample as? SRWristDetection else { return }
let isOnWrist = wrist.onWrist
let onDate = wrist.onWristDate
let offDate = wrist.offWristDate
// Watch configuration
switch wrist.wristLocation {
case .left:
print("Watch on left wrist")
case .right:
print("Watch on right wrist")
@unknown default:
break
}
switch wrist.crownOrientation {
case .left:
print("Crown on left")
case .right:
print("Crown on right")
@unknown default:
break
}
print("On wrist: \(isOnWrist)")
}Speech Metrics
SRSpeechMetrics provides audio level, speech recognition, sound classification, and speech expression data from Siri and phone calls:
func processSpeechMetrics(_ result: SRFetchResult<AnyObject>) {
guard let metrics = result.sample as? SRSpeechMetrics else { return }
let sessionID = metrics.sessionIdentifier
let timestamp = metrics.timestamp
let timeSinceStart = metrics.timeSinceAudioStart
// Audio level
if let audioLevel = metrics.audioLevel {
let loudness = audioLevel.loudness
let timeRange = audioLevel.timeRange
print("Audio level: \(loudness) dB")
}
// Speech expression (mood/valence analysis)
if let expression = metrics.speechExpression {
let confidence = expression.confidence
let mood = expression.mood
let valence = expression.valence
let activation = expression.activation
let dominance = expression.dominance
print("Expression -- mood: \(mood), valence: \(valence), confidence: \(confidence)")
}
// Speech recognition results
if let recognition = metrics.speechRecognition {
let text = recognition.bestTranscription.formattedString
print("Recognized: \(text)")
}
// Sound classification
if let classification = metrics.soundClassification {
for result in classification.classifications {
print("Sound: \(result.identifier) (\(result.confidence))")
}
}
}Face Metrics
SRFaceMetrics provides face anchor data and expression analysis. Requires a device with a TrueDepth camera (Face ID).
func processFaceMetrics(_ result: SRFetchResult<AnyObject>) {
guard let face = result.sample as? SRFaceMetrics else { return }
let sessionID = face.sessionIdentifier
let context = face.context
// Context indicates what triggered the capture
if context.contains(.deviceUnlock) {
print("Face captured during device unlock")
}
if context.contains(.messagingAppUsage) {
print("Face captured during messaging")
}
// Face expressions
for expression in face.wholeFaceExpressions {
print("Expression \(expression.identifier): \(expression.value)")
}
for expression in face.partialFaceExpressions {
print("Partial \(expression.identifier): \(expression.value)")
}
// ARKit face anchor (full blend shapes)
let anchor = face.faceAnchor
let blendShapes = anchor.blendShapes
if let smile = blendShapes[.mouthSmileLeft] {
print("Left smile: \(smile)")
}
}Wrist Temperature
The .wristTemperature stream returns SRWristTemperatureSession samples. Each session contains SRWristTemperature readings.
func processWristTemperature(_ result: SRFetchResult<AnyObject>) {
guard let session = result.sample as? SRWristTemperatureSession else { return }
print("Temperature session: \(session.startDate), duration: \(session.duration)s")
for temp in session.temperatures {
let timestamp = temp.timestamp
let value = temp.value // Measurement<UnitTemperature>, in Celsius
let error = temp.errorEstimate // Measurement<UnitTemperature>
// Check conditions that affect accuracy
let condition = temp.condition
if condition.contains(.offWrist) {
print("Off wrist -- skip reading")
continue
}
if condition.contains(.onCharger) {
print("On charger -- reduced accuracy")
}
if condition.contains(.inMotion) {
print("In motion -- reduced accuracy")
}
let celsius = value.converted(to: .celsius).value
let errorC = error.converted(to: .celsius).value
print("Temp at \(timestamp): \(celsius)C +/- \(errorC)C")
}
}Electrocardiogram and PPG
ECG Data
func processECG(_ result: SRFetchResult<AnyObject>) {
guard let samples = result.sample as? [SRElectrocardiogramSample] else { return }
for sample in samples {
let frequency = sample.frequency
let session = sample.session
let isGuided = session.sessionGuidance == .guided
// ECG voltage data points -- skip invalid readings
for dataPoint in sample.data {
guard !dataPoint.flags.contains(.signalInvalid) else { continue }
let microvolts = dataPoint.value.converted(to: .microvolts).value
print("ECG: \(microvolts) uV, guided: \(isGuided), crown: \(dataPoint.flags.contains(.crownTouched))")
}
}
}PPG Data
func processPPG(_ result: SRFetchResult<AnyObject>) {
guard let samples = result.sample as? [SRPhotoplethysmogramSample] else { return }
for sample in samples {
// Usage: .foregroundHeartRate, .foregroundBloodOxygen, .deepBreathing, .backgroundSystem
for usage in sample.usage {
print("PPG usage: \(usage)")
}
// Optical sensor data with signal quality checks
for optical in sample.opticalSamples {
let wavelength = optical.nominalWavelength
let reflectance = optical.normalizedReflectance
let hasIssues = optical.conditions.contains {
$0 == .signalSaturation || $0 == .unreliableNoise
}
if !hasIssues, let reflectance {
print("Reflectance: \(reflectance) at \(wavelength)")
}
}
}
}SRAbsoluteTime Utilities
SRAbsoluteTime wraps CFAbsoluteTime for SensorKit time ranges:
let now = SRAbsoluteTime.current()
let twoDaysAgo = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400 * 2)
let cfTime = now.toCFAbsoluteTime()
let date = Date(timeIntervalSinceReferenceDate: cfTime)
func buildWeekFetchRequest(for device: SRDevice) -> SRFetchRequest {
let request = SRFetchRequest()
request.device = device
request.from = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400 * 7)
request.to = SRAbsoluteTime(CFAbsoluteTimeGetCurrent() - 86400)
return request
}Deletion Records
The framework deletes sensor data for various reasons. Handle SRDeletionRecord in the fetch results delegate:
func processDeletionRecord(_ result: SRFetchResult<AnyObject>) {
guard let deletion = result.sample as? SRDeletionRecord else { return }
// Reasons: .userInitiated, .systemInitiated, .lowDiskSpace, .ageLimit, .noInterestedClients
print("Data deleted (\(deletion.reason)): \(deletion.startTime) to \(deletion.endTime)")
}Testing Considerations
SensorKit has significant constraints for testing:
- No Simulator support. SensorKit requires physical hardware. All testing
must happen on device.
- Entitlement required. Without the Apple-granted entitlement, the framework
returns SRError.invalidEntitlement for all operations.
- 24-hour data delay. Newly recorded data is unavailable for 24 hours.
Automated test flows must account for this holding period.
- User interaction required. Authorization requires the user to interact with
the Research Sensor & Usage Data sheet. This cannot be automated.
- Conditional sensor availability. Some sensors (wrist temperature, ECG, PPG)
require Apple Watch. Others (face metrics) require TrueDepth camera. Test on devices that have the sensors the study uses.
- Data volume. Keyboard metrics and device usage reports can be large. Profile
memory usage when processing bulk fetches.
- Background execution. SensorKit recording continues in the background
without special background mode configuration. The framework manages sensor activation independently of app lifecycle.
Related skills
How it compares
Pick sensorkit over general iOS skills when the app specifically integrates Apple SensorKit research APIs with entitlement and Info.plist requirements.
FAQ
Who is Sensorkit for?
Developers and software engineers working with sensorkit patterns from the skill documentation.
When should I use Sensorkit?
Access research-grade sensor data using SensorKit for approved studies. Use when an app needs SensorKit entitlement setup, Research Sensor & Usage Data authorization, ambient light, recorded motion, device usage, keyboar
Is Sensorkit safe to install?
Review the Security Audits panel on this page before installing in production.