A lightweight, whitelabel iOS library for real-time attention detection using ARKit (TrueDepth) and Vision (fallback). Drop it into any app — reader, video player, learning tool, etc. — and receive high-level attention events with zero coupling to your domain.
- Eye tracking — gaze direction, horizontal deviation
- Blink detection — distinguishes voluntary blinks from sustained eye closure
- Head pose — yaw (left/right) and pitch (up/down) via 4×4 → Euler matrix decomposition
- Adaptive baseline — slowly learns each user's normal posture; no manual calibration
- Confidence weighting — temporal trend analysis across a 1-second rolling window
- Sleepiness detection — eyes closed continuously > N seconds (configurable)
- Attention degradation — rolling 10/20/30/60s windows detect sustained inattention and emit severity levels
- Auto fallback — ARKit TrueDepth → Vision face detection on older devices
- App lifecycle — automatically pauses/resumes with app foreground/background
- iOS 16+
- Swift 5.9+
NSCameraUsageDescriptionin your app's Info.plist
// Package.swift
.package(url: "https://github.com/your-org/attention-engine", from: "0.1.0")Or add via Xcode: File → Add Packages…
import AttentionEngine
@MainActor
class MyViewController: UIViewController, AttentionEngineDelegate {
let engine = AttentionEngine(config: .init(sensitivity: .medium, wellnessEnabled: true))
override func viewDidLoad() {
super.viewDidLoad()
engine.delegate = self
engine.start()
}
// MARK: - AttentionEngineDelegate
func attentionEngineDidLoseAttention(_ engine: AttentionEngine) {
// e.g. pause video, dim screen
}
func attentionEngineDidRestoreAttention(_ engine: AttentionEngine) {
// e.g. resume video
}
func attentionEngineDetectedSleepiness(_ engine: AttentionEngine) {
// e.g. show "Take a break?" alert
}
func attentionEngineDetectedDegradation(_ engine: AttentionEngine,
level: AttentionDegradationLevel,
lastGoodAttentionTime: Date) {
// e.g. offer to rewind to lastGoodAttentionTime
}
func attentionEngineStatusChanged(_ engine: AttentionEngine, isActive: Bool, mode: AttentionDetectionMode) {
print("Detection \(isActive ? "started" : "stopped") using \(mode)")
}
}engine.publisher
.receive(on: DispatchQueue.main)
.sink { event in
switch event {
case .attentionLost: pausePlayback()
case .attentionRestored: resumePlayback()
case .sleepinessDetected: showBreakAlert()
case .degradation(let lvl, let since): rewindTo(since)
case .statusChanged: break
}
}
.store(in: &cancellables)struct ContentView: View {
@StateObject private var engine = AttentionEngine()
var body: some View {
Circle()
.fill(engine.isAttentionLost ? .red : .green)
.frame(width: 12, height: 12)
.onAppear { engine.start() }
.onDisappear { engine.stop() }
}
}let config = AttentionConfig(
sensitivity: .high, // .low | .medium | .high
awayThreshold: 0.1, // seconds before attentionLost fires
resumeThreshold: 0.05, // seconds before attentionRestored fires
wellnessEnabled: true, // enable sleepiness detection
sleepinessEyeClosedDuration: 3.0,
sleepinessEventCount: 3, // events before alert
degradationEnabled: true
)| Level | Away | Resume | Blink | Gaze |
|---|---|---|---|---|
| low | 250 ms | 150 ms | 0.75 | 0.40 |
| medium | 150 ms | 100 ms | 0.65 | 0.30 |
| high | 100 ms | 50 ms | 0.55 | 0.20 |
| Event | When |
|---|---|
attentionLost |
User looks away / eyes close beyond thresholds |
attentionRestored |
User looks back |
sleepinessDetected |
Eyes closed continuously ≥ sleepinessEyeClosedDuration × sleepinessEventCount |
degradation(level, lastGoodAttentionTime) |
Rolling window drops below quality threshold |
statusChanged(isActive, mode) |
Detection started/stopped, mode changed |
| Level | Window condition |
|---|---|
low |
30s avg < 0.70 and 60s avg < 0.75 |
veryLow |
20s avg < 0.60 and 30s avg < 0.65 |
critical |
10s avg < 0.55 and 20s avg < 0.60 |
MIT