minulo は、公式の Instagram や YouTube を使った時間を DeviceActivity のしきい値の合図(eventDidReachThreshold)で数え、1日の上限に届いたらスクリーンタイムでそのアプリを止めています。上限は毎朝5時に戻ります。この記事は、minulo を1人で作っている北林が書いています。
スクリーンタイムの「制限を無視」との違いは、こちらの頁にまとめています:https://minulo.app/app-limit-ignore-button.html?ref=qiita-1101b
前提
- minulo は SNS の公式サイトを WKWebView で開き、リールやショートを隠して見せるアプリです。1日の上限は、minulo の中で見た時間と、公式アプリで使った時間を足して比べます(Pro の機能です)。
- 公式アプリで使った時間そのものは、アプリ本体からは読めません。分かるのは「このしきい値に届いた」という合図だけです。
- ネイティブ側は react-native-device-activity を使い、拡張(DeviceActivityMonitor)が実行する動作は、アプリ本体が App Group の UserDefaults に書いておきます。
全体の形
見張り(DeviceActivityName)を2つ使います。
| 見張り | 時間帯 | くり返し | 合図 |
|---|---|---|---|
| 毎日の見張り | 朝5:00〜翌朝4:59 | する | 5分ごとの u5・u10…(数えるだけ)と、上限そのものの cap |
| 今日だけの見張り | 今日の朝5時〜明日の4:59 | しない | 「上限 − 今日 minulo の中で使った分」の cap |
const th = (m: number) => ({ hour: Math.floor(m / 60), minute: m % 60 });
const events = [];
for (let n = 5; n < limit; n += 5) {
events.push({ eventName: `u${n}`, familyActivitySelection: token, threshold: th(n), includesPastActivity: true });
}
events.push({ eventName: 'cap', familyActivitySelection: token, threshold: th(limit), includesPastActivity: true });
DA.startMonitoring(LIMIT_ACTIVITY, {
intervalStart: { hour: 5, minute: 0, second: 0 },
intervalEnd: { hour: 4, minute: 59, second: 0 },
repeats: true,
}, events);
includesPastActivity: true を付けているので、昼に張り直しても、その日のそれまでの分が数えに入ります。cap の合図では、手のブロックと同じブロックリストの組を遮蔽します。
つまずき1:毎日の見張りに「今日の分を引いたしきい値」を入れてはいけない
最初は、毎日の見張りの cap に「上限 − 今日 minulo の中で使った分」を入れていました。ところが、くり返す見張りなので、その値が翌朝以降も残ります。前の夜に minulo の中で50分見た人は、次の日、公式アプリを10分使っただけで丸一日止まりました。
いまは、毎日の見張りの cap は上限そのものにして、今日の分を引いた値は「今日だけ」の見張り(くり返さない)に入れています。今日 minulo の中で使っていなければ、今日だけの見張りは張りません。失敗の印も2つに分けました。同じ印にしていたら、今日だけの方の失敗で、毎日の方を20秒ごとに張り直し続けていたためです。
つまずき2:休憩中だと cap の遮蔽が効かない
minulo の「休憩」は、ブロックリストの組を白リストに入れて一時的に開けます。上限に届く前に休憩を始めていると、cap で遮蔽しても白リストが勝ち、休憩が終わるまで上限を越えて使えました。
cap の動作は、先に白リストを畳み、常に許可するアプリだけ入れ直してから遮蔽する順にしています。
actions: [
{ type: 'clearWhitelistAndUpdateBlock' },
...(alwaysAllow ? [{ type: 'addSelectionToWhitelist', familyActivitySelection: { activitySelectionId: ALLOW_ID } }] : []),
{ type: 'blockSelection', familyActivitySelectionId: id },
]
つまずき3:朝5時に外すとき、ほかのブロックまで外さない
上限の遮蔽は手のブロックと同じ組なので、朝5時の「始まり」でただ外すと、手で入れた2時間ブロックや夜の予定まで外れてしまいます。そこで、5時の始まりの動作に印(minuloLimitDay)を付け、拡張が次の条件を全部確かめたときだけ通しています。迷ったら外しません(次に minulo を開いたときにアプリ本体が外します)。
- 5:00〜5:15 の間であること。見張りを張り直した瞬間にも「始まり」が呼ばれるので、その回を通さないためです。
- 今日はまだ使い切っていなくて、前の日に使い切っていたこと。
- 手のブロックが入っていないこと、どの予定の時間帯でもないこと。これはアプリ本体が App Group に書く「いま押さえているもの」の記録で見ます。6日より古い記録は信じません。
- 記録を書いたときと地域が同じであること。
地域は、UTC との差ではなく地域の名前(TimeZone.identifier)で比べています。差で比べていたら、夏時間の切り替えの日に「地域が変わった」と読んで、外すべき朝に外しませんでした。
let sameName = (o["tzName"] as? String).map { !$0.isEmpty && $0 == TimeZone.current.identifier } ?? false
if !sameName, let tz = (o["tz"] as? NSNumber)?.intValue,
tz != TimeZone.current.secondsFromGMT(for: now) / 60 { return false }
名前が違うときだけ差で比べ直すのは、同じ地域でも表記がゆれることがあるからです。
つまずき4:アプリを閉じている間に上限に届いた日
上限に届いたとき minulo が閉じていると、アプリ本体は「今日は使い切った」という印をまだ書いていません。拡張は、cap の合図の時刻が App Group に残っているのを見て、今日の朝5時より後なら「使い切った日」と扱います。ブロック画面の文言(朝5時までの一言・休憩ボタンを出さない)もこれで決めています。
また、1日の上限は Pro の機能なので、体験期間が終わった時刻をアプリ本体が書いておき、それを過ぎたら拡張は「使い切った」と言わないようにしています。
まとめ
- くり返す見張りには、その日だけの値を入れない。今日だけの値は、くり返さない見張りに分ける。
- 同じ組を複数の仕組みで遮蔽しているときは、外す側で「ほかに押さえているものがないか」を確かめる。迷ったら外さない。
- 張り直すと始まりがその場で呼ばれる。時刻の窓で、その回を見分ける。
- 地域は名前で比べる。
minulo のリールやショートを隠す機能は無料で、1日の上限は Pro の機能です。