Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
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.
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)")
}
}
}
}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)")
}
}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))")
}SRKeyboardMetrics provides extensive typing analytics:
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")
}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)")
}
}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 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")
}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
}
}
}
}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)")
}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)")
}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)")
}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
}
}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)")
}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))")
}
}
}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)")
}
}SRWristTemperatureSession provides temperature readings collected during sleep:
func processWristTemperature(_ result: SRFetchResult<AnyObject>) {
guard let session = result.sample as? SRWristTemperatureSession else { return }
let startDate = session.startDate
let duration = session.duration
print("Temperature session: \(startDate), duration: \(duration)s")
for temp in session.temperatures {
let timestamp = temp.timestamp
let value = temp.value // Measurement<UnitTemperature>
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: \(celsius)C +/- \(errorC)C")
}
}func processECG(_ result: SRFetchResult<AnyObject>) {
guard let sample = result.sample as? SRElectrocardiogramSample else { return }
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, crown: \(dataPoint.flags.contains(.crownTouched))")
}
}func processPPG(_ result: SRFetchResult<AnyObject>) {
guard let sample = result.sample as? SRPhotoplethysmogramSample else { return }
// 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 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
}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)")
}SensorKit has significant constraints for testing:
SRError.invalidEntitlement for all operations.skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references