0
0

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 アプリを止める」を作る:今夜だけ解除と、時間帯の途中で張り直したときの落とし穴(iPhone)

0
Posted at

minulo は、寝る時間になると公式の Instagram・YouTube・TikTok などのアプリを毎晩自動で止め、リールとショートは消したまま DM は残す iPhone アプリです。リールとショートを消すのは無料で、公式アプリを止めるのは1回2時間まで無料、毎晩決めた時間に自動で始まるスリープモードは minulo Pro の機能です。この記事では、その「毎晩自動で止める」をスクリーンタイムの API(DeviceActivity と ManagedSettings)でどう作ったかと、作ってみてはまったところを書きます。(この記事は minulo の開発者が書いています)

API のくわしい挙動は Apple の開発者向けドキュメントで確かめてください。ここに書くのは minulo で実際に使っている形と、実機で起きたことです。拡張の雛形は Robert Herber さんの react-native-device-activity を元にしています。

全体の形

  • アプリ本体(React Native)が、寝る時間と起きる時間から DeviceActivitySchedule を作り、DeviceActivityCenter で見張りを始める。
  • 時間になると、OS が DeviceActivityMonitor の拡張を intervalDidStart で呼ぶ。拡張が ManagedSettings でシールド(ブロック画面)をかける。
  • 起きる時間に intervalDidEnd が呼ばれ、拡張がシールドを外す。
  • アプリが閉じていても、拡張は OS から呼ばれる。

拡張は、その場で何をするかを自分では決めません。アプリ本体が「この見張りの始まりにはこれをする」という動作を、App Group の UserDefaults に actions_for_<見張りの名前>_<合図> の鍵で書いておき、拡張はそれを読んで実行するだけにしています。拡張はメモリが小さく、合図ごとに短い時間しか動けないので、重い判断を持ち込まないためです。

見張りの名前を決めておく

見張りには名前(DeviceActivityName)が要ります。minulo では、スリープモードは1つの決まった名前、ふつうの予定は fm.sch.<予定の id>、曜日ごとに分けた予定は fm.sch.<予定の id>.<曜日> にしています。拡張は名前から予定の id を取り出して、その予定についての判断(次の節の「今夜だけ解除」)をします。

func scheduleId(_ activityName: String) -> String? {
  if activityName == SLEEP_ACTIVITY { return "bed" }
  guard activityName.hasPrefix("fm.sch.") else { return nil }
  let rest = String(activityName.dropFirst("fm.sch.".count))
  if let dot = rest.lastIndex(of: "."), Int(rest[rest.index(after: dot)...]) != nil {
    return String(rest[..<dot])  // 末尾の曜日を落とす
  }
  return rest
}

今夜だけ解除する

「今夜だけ解除する」を押したとき、見張りそのものを止めてしまうと、翌日からの毎晩の予定も消えます。そこで見張りは残したまま、「この予定のこの回は終えた」という印だけを書くことにしました。

  • アプリ本体が、予定の id と「その回が明ける時刻」を JSON にして App Group に書く。
  • 拡張は intervalDidStart のたびにその印を読み、終えた回なら何もしない。
  • 明ける時刻を過ぎた印は読むときに捨てる。
override func intervalDidStart(for activity: DeviceActivityName) {
  super.intervalDidStart(for: activity)
  var actions = loadActions(activity.rawValue, "intervalDidStart")
  if let id = scheduleId(activity.rawValue), skips()[id] != nil {
    actions = []  // 今夜は解除済み
  }
  run(actions)
}

はまったところ1:時間帯の途中で張り直すと、その場で「始まり」が呼ばれる

予定を編集したときや、何かの理由で見張りを張り直したとき、その時間帯の途中だと、OS はその場で intervalDidStart を呼びます。minulo では、緊急の解除(非常口)で見張りが止まった夜に見張りを張り直したところ、OS がその場で始まりを呼び、解除したはずの夜にもう一度全部止まる、ということが起きました。

上の「今夜だけ解除」の印を、張り直しのときの intervalDidStart でも同じように見るようにして直しました。スリープモードの見張りも、予定と同じ印(bed)で見るようにしています。

はまったところ2:終わりの合図で、ほかの見張りのシールドまで外れる

ManagedSettings のシールドは、見張りごとに別々の箱があるわけではありません。夜の見張りの intervalDidEnd でシールドを外すと、同じ時間に別の理由(手で始めた2時間のブロックなど)でかけていたシールドまで外れることがあります。

そこで、終わりの合図で外したあとに「まだ必要な人がいれば掛け直す」動作を一緒に登録し、その動作に「誰のための掛け直しか」(予定の id や、手のブロックが終わる時刻)を持たせました。拡張は、全員が終えた回なら掛け直さず、まだ終わっていない人がいれば掛け直します。

はまったところ3:時刻はその地域の時計で決まる

DeviceActivitySchedule の時刻は、端末の地域の時計で動きます。旅行で地域が変わると、アプリ本体が書いた「この時間帯は予定の中」という印と、拡張が見ている今の時刻がずれます。minulo では、印に書いたときの地域の名前を入れておき、名前が違えば拡張は迷ったほうを選ばず何もしない(次にアプリを開いたときに本体が直す)ようにしました。夏時間の切り替え日に「地域が変わった」と誤って読んだことがあったので、比べるのは UTC との差ではなく地域の名前にしています。

できないこと

  • 止めている間、公式アプリの通知は届きません。シールドは iOS の仕組みなので、アプリ側では変えられません。
  • シールドは公式アプリそのものを止めるので、「リールだけ止めて DM は残す」は公式アプリの中ではできません。minulo では、DM は minulo の中でウェブ版を開いて使ってもらう形にしています。

まとめ

  • 毎晩の自動のブロックは、DeviceActivity の見張りと、拡張が読む「動作の表」で作れる。
  • 「今夜だけ解除」は見張りを止めずに、終えた回の印で作る。
  • 張り直しの瞬間の intervalDidStart、シールドの共有、地域の時計の3つは、実機で必ず一度はまる。

寝る前のリールとショートを止める設定全体(休止時間やスクリーンタイムのスケジュールとの違い)は、次のページにまとめています。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?