DeviceActivity sur iOS : un blocage qui se lève à l'heure prévue, et les cinq pièges d'horaire que j'ai rencontrés
Bonjour, je m'appelle Ayumu Kitabayashi et je développe seul minulo , une app iPhone qui masque les Reels d'Instagram, les Shorts de YouTube et le fil « Pour toi » de TikTok, tout en gardant les messages privés. L'app est écrite avec Expo / React Native. Pour la partie Temps d'écran, j'utilise la b

Bonjour, je m'appelle Ayumu Kitabayashi et je développe seul minulo, une app iPhone qui masque les Reels d'Instagram, les Shorts de YouTube et le fil « Pour toi » de TikTok, tout en gardant les messages privés. L'app est écrite avec Expo / React Native. Pour la partie Temps d'écran, j'utilise la bibliothèque react-native-device-activity de Robert Herber, complétée par mon propre code.
minulo propose un blocage minuté: on verrouille l'app officielle Instagram avec Temps d'écran pendant, par exemple, 30 minutes, et elle doit se débloquer toute seule à l'heure prévue, même si minulo n'est pas lancée. J'ai buté cinq fois, et à chaque fois sur la même chose: la façon dont iOS interprète le début et la fin d'une plage horaire.
Le code ci-dessous est en TypeScript, mais il appelle en dessous DeviceActivityCenter.startMonitoring avec un DeviceActivitySchedule. Les mêmes problèmes se posent donc si vous écrivez directement en Swift.
Pour le point 2, l'app ne peut pas compter sur elle-même: elle est peut-être suspendue. On réserve donc une plage intervalStart – intervalEnd dans un DeviceActivitySchedule, et c'est l'extension DeviceActivityMonitor qui lève le blocage dans intervalDidEnd.
Si l'on passe une plage de 5 minutes à startMonitoring, on obtient une erreur. Lors de mes tests sur appareil, tout ce qui faisait moins de 15 minutes était refusé.
Ma première parade était « maintenant + 15 minutes ». Résultat: une pause de 5 minutes restait ouverte 15 minutes. La bonne solution consiste à garder la fin fixe et à reculer le début:
// on garde la fin; on recule le début pour que la plage fasse 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 ); // si le début tombe la veille, on ajoute la date (voir le point 3) return { s, e, dated: s. getDate ()!== now. getDate () }; } Enter fullscreen mode Exit fullscreen mode La fin est arrondie à la minute supérieure. Le début, lui, se retrouve alors un peu dans le passé; ce qui compte pour moi, c'est que intervalDidEnd arrive à l'heure de fin.
Avec des DateComponents qui ne contiennent que l'heure et la minute, un blocage de 24 heures (de 13 h 05 aujourd'hui à 13 h 05 demain) se lit comme « début = fin », et la plage est refusée.
Je décale donc le début d'une minute plus tard, ce qui donne une plage de 23 h 59. Le décaler plus tôt aurait donné une plage d'une minute, refusée à cause du point 1.
if ( s. getHours () === e. getHours () && s. getMinutes () === e. getMinutes ()) { s = new Date ( s. getTime () + 60000 ); } Enter fullscreen mode Exit fullscreen mode 3. Juste après minuit, l'heure et la minute ne suffisent plus À cause du point 1, une pause qui se termine à 0 h 05 commence à 23 h 50. Envoyé avec seulement l'heure et la minute, ce 23 h 50 était lu comme « 23 h 50 aujourd'hui », et la plage devenait incohérente. Le symptôme: une pause de 5 minutes prise peu après minuit restait ouverte 15 minutes.
Quand le début tombe la veille, et seulement dans ce cas, j'ajoute maintenant l'année, le mois et le jour:
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 }; } Enter fullscreen mode Exit fullscreen mode 4. Le nombre d'activités surveillées est limité minulo gère aussi des créneaux récurrents (l'heure du coucher, les heures de cours…). Si l'on enregistre une activité ( DeviceActivityName ) par jour de la semaine et par créneau, le compte grimpe vite. Le nombre d'activités surveillées serait plafonné à une vingtaine environ. Au-delà, startMonitoring échoue, et le blocage ne se lève tout simplement jamais à l'heure.
DA. startMonitoring ( FOCUS_ACTIVITY, { intervalStart, intervalEnd, repeats: false }, []). catch (( x ) => set ( ' focus:nativeErr ', String ( x?. message?? x ). slice ( 0, 120 ))); Enter fullscreen mode Exit fullscreen mode Le.catch vide est tentant. Ici, il cache exactement le cas où l'utilisateur reste bloqué.
Dans intervalDidEnd, retirer tous les boucliers enlevait aussi d'autres blocages censés rester actifs à ce moment-là: un créneau de coucher, ou une app dont la limite quotidienne était déjà atteinte. Concrètement, un blocage manuel qui se terminait en pleine nuit emportait avec lui le blocage du coucher, qui devait durer jusqu'au matin.
Désormais, l'action de fin réapplique ce qui doit encore être actif à l'heure de fin. La liste est calculée au moment où l'on lance le blocage:
DA. configureActions ({ activityName: FOCUS_ACTIVITY, callbackName: ' intervalDidEnd ', actions: [ { type: ' resetBlocks ' }, // on réapplique les créneaux encore en cours à l'heure de fin... during. map (( id ) => ({ type: ' blockSelection ', familyActivitySelectionId: id })), ] }); Enter fullscreen mode Exit fullscreen mode Côté extension, je filtre aussi les réapplications qui appartiennent à des créneaux déjà terminés, pour qu'un enregistrement répété ne les traîne pas jusqu'au lendemain.
Les cinq points ont un point commun: DeviceActivitySchedule ne connaît que ce qu'on lui donne dans les DateComponents, et une heure sans date est ambiguë dès qu'on approche de minuit ou de 24 heures. Si vous construisez des plages à partir d'une durée plutôt que d'une heure fixe, je vous conseille de tester les cas « 5 minutes », « 24 heures » et « juste après minuit » dès le début.
Masquer les Reels, les Shorts et le fil de TikTok dans minulo est gratuit. On peut aussi bloquer les apps officielles avec Temps d'écran; en gratuit, c'est jusqu'à deux heures par blocage. Pendant qu'une app officielle est bloquée, ses notifications n'arrivent pas: c'est ainsi que minulo fonctionne. Les messages privés restent accessibles dans minulo.
Si vous avez rencontré d'autres pièges avec DeviceActivity, je suis preneur de vos retours en commentaire.
https://minulo.app/fr/?ref=devto-fr-1022