I build minulo, an iPhone app that hides Reels and Shorts. One feature is a timed block: lock the official Instagram app with Screen Time for, say, 30 minutes, and have it unlock on its own even if minulo isn't running. That part runs on DeviceActivity (DeviceActivityCenter.startMonitoring + a DeviceActivitySchedule, with the unlock happening in a DeviceActivityMonitor extension's intervalDidEnd).
It took five rounds of bugs to get the timing right. All five are about how the schedule's start and end are interpreted, so they apply whether you write Swift directly or go through a wrapper like react-native-device-activity (which I use, with my own additions).
1. Windows shorter than 15 minutes are rejected
Pass a 5-minute window to startMonitoring and you get an error. In my testing on device, anything under 15 minutes was refused.
My first workaround was "now + 15 minutes", which meant a 5-minute break stayed unlocked for 15. The fix was to keep the end fixed and move the start back instead:
// keep the end; pull the start back so the window is 15 minutes
export function shortWindow(untilMs: number) {
const now = new Date();
const e = new Date(Math.ceil(Math.max(untilMs, now.getTime() + 60000) / 60000) * 60000);
if (e.getTime() - now.getTime() >= 15 * 60000) return { s: now, e, dated: false };
const s = new Date(e.getTime() - 15 * 60000);
// if the start falls on the previous day, include the date (see #3)
return { s, e, dated: s.getDate() !== now.getDate() };
}
A start in the past is fine; intervalDidEnd still fires at the end.
2. Same hour and minute for start and end is rejected
With hour/minute-only DateComponents, a 24-hour block (13:05 today to 13:05 tomorrow) reads as start == end and is refused. I shift the start one minute later, giving a 23h59m window. Shifting it earlier would make a 1-minute window, which runs into #1.
if (s.getHours() === e.getHours() && s.getMinutes() === e.getMinutes()) {
s = new Date(s.getTime() + 60000);
}
3. Just after midnight, hour/minute isn't enough
Because of #1, a break ending at 00:05 starts at 23:50. Sent as hour and minute only, that was read as 23:50 today, and the window broke: a 5-minute break after midnight stayed open for 15. When the start falls on the previous day, I now include year, month and day:
export function winEdge(d: Date, dated: boolean) {
return dated
? { year: d.getFullYear(), month: d.getMonth() + 1, day: d.getDate(), hour: d.getHours(), minute: d.getMinutes(), second: 0 }
: { hour: d.getHours(), minute: d.getMinutes(), second: 0 };
}
4. The number of monitored activities is limited
Registering one activity per weekday for each schedule (bedtime, class hours…) adds up fast, and the number of monitored activities is reportedly capped at around 20. Past that, startMonitoring fails, and the block simply never unlocks. Two changes:
- daily schedules are a single activity without a weekday
- failures are stored instead of swallowed, so the app can show that something is wrong
DA.startMonitoring(FOCUS_ACTIVITY, { intervalStart, intervalEnd, repeats: false }, [])
.catch((x) => set('focus:nativeErr', String(x?.message ?? x).slice(0, 120)));
5. "Unblock everything" also unblocks things that should stay blocked
Clearing all shields in intervalDidEnd also removed other blocks that should still be active at that moment, like a bedtime schedule or an app whose daily limit was used up. A manual block ending in the middle of the night took the bedtime block down with it until morning.
Now the end action re-applies whatever should still be active at that time, computed when the block starts:
DA.configureActions({ activityName: FOCUS_ACTIVITY, callbackName: 'intervalDidEnd', actions: [
{ type: 'resetBlocks' },
// re-apply schedules that are still in their window at the end time
...during.map((id) => ({ type: 'blockSelection', familyActivitySelectionId: id })),
] });
The extension also drops re-applies that belong to schedules already finished, so a repeating registration doesn't carry them into the next day.
Summary
- Windows under 15 minutes (on device) are refused: fix the end, move the start back.
- Identical start and end hour:minute are refused: move the start one minute later.
- After midnight the start lands on the previous day: include the date only then.
- Monitored activities are capped: merge daily schedules and record failures.
- Clearing everything clears too much: re-apply what should stay, at the end time.
Related: opening your own app from a Screen Time block screen (ShieldAction can't open URLs, so I go through a notification).
minulo (hiding Reels and Shorts is free): https://minulo.app/en/?ref=devto-1021b
List of apps and methods for hiding Reels and Shorts on iPhone: https://github.com/ayumu-kitabayashi/awesome-reels-shorts-blockers
Top comments (0)