1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DeviceActivity で「SNS は1日◯分まで」を作る:毎日の見張りと今日だけの見張りを分け、朝5時に外す(iPhone)

1
Posted at

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 の機能です。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?