DEV Community

Todd Sullivan
Todd Sullivan

Posted on

When Your On-Device Model Decides How Often to Ping You

Most notification systems are dumb. They fire on a fixed schedule decided by a developer who's never met the user. Every day at 9am. No matter what.

I've been building an iOS health app that does something different: the on-device ML model's training state drives notification frequency. The more the model knows about you, the less it needs to ask.

The Problem With Fixed Schedules

For a health tracking app, consistent data collection is everything — it's the training set. Early on, you need daily check-ins to bootstrap the model. But once the model is trained and the user has established patterns, daily prompts become noise. People turn off notifications. You lose signal entirely.

The naive fix is "let users choose their own frequency." That works until users pick "weekly" in week one when the model is still blind.

The better fix: make the frequency a function of the data itself.

The Logic

I built a NotificationFrequencyManager that recomputes check-in frequency after every engagement cycle and after every model training run. Three signals go in:

  1. Data volume — how many days of logs exist
  2. Logging consistency — what percentage of the last 14 days has at least one entry (≥70% threshold to step down)
  3. ML training state — is the personalised model untrained, training, or trained?

The output maps to three frequencies:

enum NotificationFrequency: Int, Codable, Comparable {
    case high   = 1  // Daily — new users, untrained model
    case medium = 2  // Every 2 days — moderate data, some consistency
    case low    = 4  // Every 4 days — trained model, high consistency
}
Enter fullscreen mode Exit fullscreen mode

There's a hard floor at 4 days. Even the most consistent user with a fully trained model gets nudged at least every 4 days. The model still needs new data to stay fresh. Silence would mean drift.

Closing the Loop

The clever part is that ML training completion directly triggers a frequency recalculation:

func update(logs: [DailyLog], trainingState: TrainingState) -> NotificationFrequency {
    let frequency = compute(logs: logs, trainingState: trainingState)
    currentFrequency = frequency
    storedFrequency = frequency  // persisted to App Group UserDefaults
    return frequency
}
Enter fullscreen mode Exit fullscreen mode

So the flow is: user logs data → engagement manager runs → model retrains → frequency updates → next notification scheduled at new interval. The app literally quiets down as it learns.

Smooth Transitions

One thing I got wrong in early iterations: jumping straight from daily to every-4-days felt jarring in testing. The fix was requiring both consistency and data volume thresholds before stepping down. You can't hit low unless you've been on medium for at least a week.

// Only step from medium → low if model is trained AND consistency is high
if dataVolume >= 30 && consistency >= 0.7 && trainingState == .trained {
    return .low
}
Enter fullscreen mode Exit fullscreen mode

Why This Matters

The insight generalises well beyond health apps. Any time you have an adaptive ML component, the model's confidence or training state is a signal you can use to drive other app behaviour. Frequency of prompts. Depth of UI. Whether to show explanations or trust the user already gets it.

The model knowing you means the app behaves differently for you. That's actually what "personalised AI" should mean — not just personalised outputs, but personalised interactions.

Top comments (0)