← All work

Deep Score

A sleep score of my own, built on Apple Watch data. It weights deep sleep the way I want and keeps Apple's Sleep Score alongside for comparison.

Overview

I've slept with an Apple Watch every night for over a year, and Health's Sleep Score is the first thing I check in the morning. It's based on how long I slept, when I went to bed compared to usual and how often I woke up. Sleep stages don't factor in, so a night with very little deep sleep can still score high, and deep sleep is what I'm trying to improve.

Deep Score uses the same Watch data from HealthKit but scores each night against my own targets, with deep sleep weighted however I want. I still log Apple's score each morning, and the app estimates it too, so I can compare the two.

At a glance

Role
Design and engineering
Era
2026
Platform
iOS 27
Surface
App, Home and Lock Screen widgets, Siri, morning notification
Stack
Swift 6 · SwiftUI · HealthKit · SwiftData · Swift Charts · WidgetKit · App Intents · Foundation Models (evaluated)
The Home Screen widget: a Deep Score of 88, with time asleep, deep sleep and Apple's score of 97
The morning summary on the Lock Screen: 35 minutes of deep sleep, 25 short of the target, and a score of 83
One night in Health: a Sleep Score of 90, High, with full points for bedtime
The same night in Deep Score: 84, with deep sleep costing the most

Key features

  • A score you can tune. Five parts: duration, deep sleep, REM, bedtime and interruptions. Each has a weight slider, and the total always comes out of 100. By default, duration counts for 35 points, deep sleep 25, bedtime 20, and REM and interruptions 10 each. Deep and REM targets work in minutes or as a percentage of the night.
  • Presets with sources. Four starting points, from sleep-lab norms to deep sleep first, each linked to where its numbers come from.
  • Apple's score alongside. Each night shows an estimate of Apple's score by element, next to the real score from Health once I've entered it.
  • Trends. A month, six months, a year or all time, in Swift Charts.
  • Widgets. Score, time asleep, deep sleep and Apple's score on the Home Screen and Lock Screen.
  • Siri. Ask about last night, five nights ago or how the last few weeks have gone.
  • A morning summary. A notification shortly after waking with deep sleep against the target and what moved the score most.
  • Sleep tips. Each one cites its studies and rates the evidence, including when it's weak.
Six months of weekly averages: Deep Score against Apple's score, estimated and as entered from Health

Technical highlights

  • Scoring in its own Swift package. No HealthKit or SwiftUI in it, so it's fast to test, and the app, widget and Siri share the same code.
  • Tested against real nights. Health's points for 39 of my own nights, across trips in three time zones, are in a test. If a change makes the estimate worse, the test fails.
  • Incremental sync. The first launch reads every night HealthKit has, about 730 for me. After that, an anchored query fetches only added or deleted samples, and only those nights are rebuilt, saved in one transaction.
  • One shared store. HealthKit can't be read while the phone is locked, so the app does the scoring and writes results to SwiftData in an App Group. The widget and Siri just read them.
  • Foundation Models, tried and left out. I had the on-device model write the morning summary and checked it on 13 nights in code: numbers had to come from computed facts, it had to name the score and the biggest cost, and it had to stay short. After five prompt revisions the numbers were right, but it stayed under length only 38% of the time, and the model grading itself missed the failures. The notification uses a template for now.
Siri summing up recent nights: mostly high, with a 96 on October 1, a 100 on February 7 and a low of 82 on June 19
A sleep tip with what the research found, its evidence rating and the study behind it

Challenges

Travel. Bedtime is compared to your usual bedtime, and HealthKit timestamps don't carry local time on their own. My nights in San Francisco showed up two hours late against my bedtime in Austin, and the first nights home looked early. Health was clearly using local time. Each sample records the time zone it came from, so the app now uses it. For the month around that trip, the estimate's total bedtime error dropped from 158 points to 43.

A migration that would have lost data. Adding the time zone changed the shape of saved nights, and SwiftData uses that shape to identify a store's version. My tests passed because they built the old stores with the new code. Opening an actual file from the previous build failed with "unknown model version", and the app would have fallen back to an empty history. The fix keeps a frozen copy of each old version's model, and I checked against that real file that every night came through.

What I’d improve today

Move the cross-build migration check into the regular test suite, and add cases for daylight saving and partial nights. The Apple estimate is within a few points most nights. The remaining gap looks like Health using slightly different bedtimes than HealthKit exposes, so more logged scores will help more than more modeling.