iPhone アプリ「minulo」(インスタのリールや YouTube のショートだけを消すアプリ)を一人で作っています。スクリーンタイム(FamilyControls / ManagedSettings)で公式アプリを止め、ブロック画面のボタンから自分のアプリへ移ってもらう作りにしたところ、「ブロック画面から自分のアプリを開く」が素直にはできず、いくつか回り道をしました。同じことをする人向けのメモです。
React Native(Expo)製で、スクリーンタイムまわりは react-native-device-activity(作者 Robert Herber さん)を元に、自前の手を足しています。
やりたかったこと
- スクリーンタイムで Instagram などの公式アプリを止める(ManagedSettings の shield)
- 公式アプリのアイコンを押すと、ブロック画面(ShieldConfiguration で見た目を変えたもの)が出る
- ブロック画面の主ボタン「minulo で開く」を押すと、minulo が開いてリールのない Instagram に移る
1. ShieldAction から URL を開いても何も起きない
ボタンの処理は ShieldActionDelegate の handle(action:for:completionHandler:) に来ます。ここで
let context = NSExtensionContext()
context.open(url) { _ in }
のように自分のアプリの URL スキームを開こうとしても、実機では何も起きませんでした。エラーも出ず、依頼が黙って無視されます。ブロック画面の拡張から、ほかのアプリ(自分のアプリを含む)を直接開く道は無いと考えた方がよさそうです。
2. 通知を1つ出して、それを押してもらう
代わりに、ShieldAction の中でローカル通知を1つ出し、その通知を押すとアプリが開くようにしました。
sendNotification(contents: [
"title": title, // アプリが App Group に置いた文(言語に合わせてある)
"body": body,
"userInfo": ["url": url], // アプリ側で受け取って画面を移る
"identifier": "minulo-open",// 同じ id なので前の通知は置き換わる
"interruptionLevel": "active",
], placeholders: [:])
アプリ側は通知のタップを受けて userInfo.url を開くだけです。ブロック画面でボタンを押す → すぐ上から通知が出る → 押すと minulo が開く、という2タップになります。
3. completionHandler を少し待ってから返す
completionHandler を呼ぶと拡張が片づけられるので、通知の登録が終わる前に消えてしまうことがありえます。通知を出すボタンのときだけ 0.4 秒待ってから返しています。返すのは .defer(ブロック画面は閉じない)なので、待っても画面はそのまま残ります。
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
completionHandler(.defer)
}
4. 通知が許可されていないと、ボタンは何もしない
通知を経由する以上、通知を許可していない人がボタンを押しても何も起きません。押しても無反応なボタンはいちばん悪いので、アプリ側で通知の許可状態を App Group に書いておき、許可が無いときはブロック画面のボタンを「閉じる」1つだけにしています。ShieldConfiguration と ShieldAction の両方が同じ印を読みます。
5. ShieldConfiguration からは App Group に書けない
「ブロック画面が何回表示されたか」を数えたくて、見た目を返す ShieldConfiguration の拡張から App Group の UserDefaults に書いてみましたが、iOS の囲いで黙って無視されていました(計測ツールで見ると、届いた値が全件「なし」)。読むことはできますが、書けません。回数はボタンが押された側(ShieldAction)で数えることにしました。
6. アプリ名が渡ってこないことがある
ブロック画面の文やボタンの URL に、止めたアプリの名前(Instagram など)を入れています。ところが Apple が拡張に名前を渡さないことがあるので、取れないときは空にする前提で組んでいます。置き換えの決まりによっては「applicationName」という文字がそのまま入ってしまうので注意が要ります。
7. URL に入れる文字の変換
名前を URL のクエリに入れるとき、CharacterSet.alphanumerics を使うと日本語の文字も「英数字」に含まれてしまい、変換されずに通ります。ASCII の英数字と -._~ だけを明示した集合を使っています。
let allowed = CharacterSet(charactersIn:
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")
まとめ
- ShieldAction から自分のアプリを直接は開けない → ローカル通知を経由する
- 通知を出す前に拡張が片づけられないよう、少し待ってから
.deferを返す - 通知が無いと押しても無反応 → そのときはボタンを「閉じる」だけにする
- ShieldConfiguration は App Group に書けない(読むのはできる)
アプリは App Store にあります(消す機能は無料):https://apps.apple.com/jp/app/id6809352229
同じ分野のアプリと設定の一覧:https://github.com/ayumu-kitabayashi/awesome-reels-shorts-blockers