個人開発でフォーカスタイム系のアプリを Rork Max で組んでいたとき、時間になるとブロックはかかるのに、出てくるのが Apple 既定の灰色の画面のままで、自分の作ったアプリだと気づいてもらえない、という状態が続きました。ユーザーからすると、丁寧に作ったオンボーディングを抜けた先で、急に無機質なシステム画面に突き放される感覚になります。
この「遮蔽画面」は差し替えられます。ただ、メインアプリのコードからは触れません。ブロック中の画面は別プロセスの拡張が描いているためで、そこに手を入れるには専用の App Extension を2つ足す必要があります。ひとつは見た目を差し替える ShieldConfiguration 拡張、もうひとつはその画面のボタンが押されたときの挙動を決める ShieldAction 拡張です。
前提として、Family Controls のエンタイトルメント取得と ManagedSettingsStore で実際にシールドをかける土台は済んでいるものとします。ここではその先の、画面の作り替えに絞ってお話しします。
なぜメインアプリから遮蔽画面を描けないのか
store.shield.applications にトークンを渡すと、対象アプリを開いた瞬間にシステムが遮蔽画面を差し込みます。この画面を描いているのはあなたのアプリではなく、ManagedSettingsUI の枠組みで動く別プロセスです。React Native 側からはもちろん、Swift のメインターゲットからも直接は触れません。
差し替えの入口は ShieldConfigurationDataSource の一点だけです。システムが遮蔽画面を出す直前にこのクラスへ問い合わせ、返した ShieldConfiguration の内容で画面を組み立てます。つまり私たちが決められるのは「どんな部品を、どの色で並べるか」であって、任意の SwiftUI ビューを流し込めるわけではありません。ここを勘違いすると、凝ったレイアウトを描こうとして詰まります。
| やりたいこと | できる / できない | 手段 |
|---|---|---|
| 背景色・ぼかしを変える | できる | backgroundColor / backgroundBlurStyle |
| アイコンを自分のものにする | できる(拡張に同梱した画像のみ) | icon に UIImage |
| 見出し・本文の文言と色 | できる | title / subtitle(Label) |
| ボタンを2つ置く | できる(最大2つ・ラベルと色のみ) | primaryButtonLabel / secondaryButtonLabel |
| 任意の SwiftUI ビューを描く | できない | — |
| 遠隔の画像をその場で読み込む | 実質できない | 拡張に同梱するか App Group 経由 |
ステップ1: ShieldConfiguration 拡張を追加する
Xcode で新しいターゲットとして「Shield Configuration」拡張を追加します。Rork Max のように Expo ベースでビルドしている場合は、eas build のたびに手作業でターゲットを足し直すのは現実的ではないので、config-plugin 側で拡張ターゲットと Info.plist、エンタイトルメントを注入する形にしておきます。私は本体のネイティブモジュールと同じ config-plugin にまとめて、拡張の追加漏れが起きないようにしています。
拡張の Info.plist では、NSExtensionPointIdentifier に com.apple.ManagedSettingsUI.shield-configuration-service を、NSExtensionPrincipalClass に自作クラス名を指定します。ここが違っているとビルドは通るのにシステムから呼ばれず、既定画面のまま、という無言の失敗になります。
import ManagedSettings
import ManagedSettingsUI
import UIKit
class FocusShieldConfiguration: ShieldConfigurationDataSource {
override func configuration(shielding application: Application) -> ShieldConfiguration {
return ShieldConfiguration(
backgroundBlurStyle: .systemThinMaterialDark,
backgroundColor: UIColor(red: 0.08, green: 0.10, blue: 0.16, alpha: 1),
icon: UIImage(named: "ShieldLeaf"),
title: ShieldConfiguration.Label(
text: "いまは集中の時間です",
color: .white
),
subtitle: ShieldConfiguration.Label(
text: "この時間帯は自分で選んで閉じたアプリです。あと少しだけ、続けてみませんか。",
color: UIColor(white: 0.75, alpha: 1)
),
primaryButtonLabel: ShieldConfiguration.Label(
text: "集中に戻る",
color: .white
),
primaryButtonBackgroundColor: UIColor(red: 0.20, green: 0.55, blue: 0.90, alpha: 1),
secondaryButtonLabel: ShieldConfiguration.Label(
text: "5分だけ開く",
color: UIColor(white: 0.65, alpha: 1)
)
)
}
// カテゴリ単位でブロックした場合はこちらも返す
override func configuration(shielding application: Application,
in category: ActivityCategory) -> ShieldConfiguration {
return configuration(shielding: application)
}
}secondaryButtonLabel を nil にすればボタンは1つになります。文言は具体的にしておくと効きます。「ブロック中」という事実の通知ではなく、「自分で選んだ約束だ」と思い出してもらう一言に変えるだけで、ユーザーの受け取り方がかなり変わります。私自身、文言を「アクセスがブロックされています」から上のような一文にしただけで、レビューでの評価の温度が明らかに上がりました。