Is local-time validation and re-arming a supported workaround for premature DeviceActivity thresholds on iOS 26?

I am investigating premature DeviceActivityMonitor.eventDidReachThreshold callbacks on physical devices running iOS 26.x.

I have also observed similar overcounting behavior on iOS 18.x. In one repeatable test, an all-activity event configured with a 10-minute threshold fired after approximately 5 minutes of actual unlocked usage.

The event was created with:

  • A single completion threshold.
  • includesPastActivity: false.
  • A nonrepeating DeviceActivitySchedule.
  • A unique activity and event identity for each monitoring generation.

However, eventDidReachThreshold could still arrive significantly earlier than expected.

Defensive mitigation

I have been testing a defensive mechanism that treats eventDidReachThreshold only as a wake-up signal, rather than authoritative proof that the configured usage duration has elapsed.

When the callback arrives, the monitor extension independently validates the duration against locally persisted timing state.

The flow is:

  1. Each logical work cycle has a unique cycle identifier and generation number.
  2. The app stores a local timing anchor when monitoring begins or resumes.
  3. When the threshold callback arrives, the extension verifies:
    • The cycle identifier.
    • The generation number.
    • The activity name.
    • The current application state.
  4. The extension calculates a locally trusted elapsed duration.
  5. If the local duration has not reached the configured duration:
    • It does not send a notification.
    • It does not apply any user-visible action.
    • It persists only the locally trusted progress.
    • It increments the generation number.
    • It stops the previous monitor.
    • It registers a new event for only the locally remaining duration.
  6. Completion is accepted only when the locally calculated duration is due.
  7. Delayed callbacks from previous generations are ignored.

Simplified pseudocode:

override func eventDidReachThreshold(
    _ event: DeviceActivityEvent.Name,
    activity: DeviceActivityName
) {
    let state = loadPersistedState()

    guard eventMatchesCurrentGeneration(
        event: event,
        activity: activity,
        state: state
    ) else {
        // Ignore stale or duplicated callbacks.
        return
    }

    let now = Date()
    let trustedElapsed = calculateLocallyAccountedElapsed(
        state: state,
        now: now
    )

    let tolerance: TimeInterval = 3

    if trustedElapsed + tolerance < state.configuredDuration {
        let remaining = state.configuredDuration - trustedElapsed

        var nextState = state
        nextState.confirmedElapsed = trustedElapsed
        nextState.generation += 1

        // Persist the new generation before replacing the monitor.
        persistAtomically(nextState)

        center.stopMonitoring([activity])

        let nextActivity = makeActivityName(
            cycleID: nextState.cycleID,
            generation: nextState.generation
        )

        let completionEvent = DeviceActivityEvent(
            threshold: normalizedDateComponents(remaining),
            includesPastActivity: false
        )

        do {
            try center.startMonitoring(
                nextActivity,
                during: makeNonRepeatingSchedule(),
                events: [
                    makeCompletionEventName(nextState): completionEvent
                ]
            )
        } catch {
            // Persist a recoverable unavailable state.
            recordMonitoringFailure(error)
        }

        return
    }

    transitionToCompletedState()
    scheduleUserNotification()
}

Durations are normalized before creating the event:

func normalizedDateComponents(
    _ duration: TimeInterval
) -> DateComponents {
    let seconds = max(1, Int(duration))

    return DateComponents(
        minute: seconds / 60,
        second: seconds % 60
    )
}

This avoids using values such as second: 300.

Example

For a configured duration of 10 minutes:

  1. DeviceActivity incorrectly delivers the completion callback after approximately 5 minutes.
  2. Local accounting reports only approximately 5 minutes.
  3. No notification or other user-visible action is performed.
  4. The previous monitor is replaced with a new generation configured for the remaining approximately 5 minutes.
  5. Completion is accepted only after the locally trusted timing state is due.

This mechanism has so far prevented premature DeviceActivity callbacks from producing premature notifications during my physical-device testing on iOS 26.x.

Additional precautions

The implementation also uses the following precautions:

  • Only one completion event is registered instead of multiple minute checkpoints.
  • includesPastActivity is explicitly set to false.
  • Every replacement monitor has a new generation identity.
  • Generation state is persisted before the old monitor is replaced.
  • Callbacks from an old cycle, generation, or activity name are ignored.
  • The extension performs only small, bounded state updates.
  • User-visible actions occur only after local validation succeeds.

Limitations

This is a defensive workaround, not a fix for the underlying DeviceActivity or Screen Time accounting issue.

Known limitations include:

  • It cannot prevent iOS from delivering an incorrect callback.
  • If the system never delivers another callback, completion may be delayed or missed.
  • If every new event immediately fires, repeated re-registration may occur.
  • startMonitoring may fail if the system considers the activities too numerous or too tightly scheduled.
  • Local unlocked-time accounting depends on reliable lock and unlock observations.
  • Wall-clock calculations must consider manual system-time changes.
  • The approach cannot correct Screen Time’s internal activity data.
  • For modes that intentionally count locked time, absolute local-notification scheduling may be more reliable and may avoid DeviceActivity thresholds entirely.

All processing in this mitigation occurs on-device. It does not require uploading activity tokens, Screen Time data, user identifiers, or diagnostic logs. The implementation uses only public APIs.

Questions for Apple

  1. Is treating eventDidReachThreshold as a wake-up signal and validating it against locally persisted timing state an acceptable design?
  2. Is stopping the current monitor and registering a new generation for only the locally remaining duration from the monitor extension considered a supported recovery pattern?
  3. Are there documented or recommended limits, rate controls, or backoff requirements for this type of defensive re-registration?
  4. Is there a more reliable supported API for usage-based completion when eventDidReachThreshold fires prematurely on iOS 26?

I would appreciate confirmation from Apple engineers or feedback from other developers who have tested a similar approach.

Is local-time validation and re-arming a supported workaround for premature DeviceActivity thresholds on iOS 26?
 
 
Q