【スクリーンタイム】FamilyActivityPickerの問題とワークアラウンド
はじめに
FamilyActivityPickerでは、プライバシーセンシティブな「ユーザーがどのアプリを使っているか」という情報を3rd partyからは見えないように、UIRemoteViewというプライベートクラスを内部的に利用し、XPC技術により他プロセス(デーモン)で描画したビューを表示しています。しかし、その部分に問題があって、条件によって画面がフリーズしたり真っ白になったりします。
FamilyActivityPickerとfamilyActivityPicker(isPresented:selection:)は、配下の項目件数が少ないときはカテゴリを開くのに成功しますが、100件を超えるようなアプリとwebドメインを持つカテゴリを開こうとすると何割かの確率で画面がクラッシュしはじめます。手元の環境では、200件を超えるようなカテゴリがある場合は、開こうとした時にほぼ確実に画面がクラッシュしました。
推測では、App Exntesionのように別プロセスのメモリ上限があって、それを開くアクションや検索結果の表示で超過するケースにおいてクラッシュが起きているのではないかと考えています。
Apple Developer Forumにポストを作成し、TSIのチケットも作成して尋ねてみましたが「既知のバグです」とAppleエンジニアから返答があるだけでした。これにより「認知はされているが何らかの理由 (人手が足りない、XPCの問題はFamily Controlsチームより広範な問題で手が出せない、優先度が低い、など考えられる)で直せていない問題」だということは分かりました。それはそれで仕方ないのですが、このポストを読んで同様の問題に差し掛かった人は、ぜひフィードバックアシスタント (Macアプリ版とWeb版があります)からバグ報告をしてみてください。どうやらフィードバックアシスタントの件数が対応優先度に影響を与えるらしく、多くのフィードバックがAppleに送信されるほど対応される可能性が高まります。
ワークアラウンド
XPCのクラッシュ自体を回避することはできませんが、エラー表示を迅速にユーザーに提示する方法はあります。FamilyActivityPickerはクラッシュすると透過状態に移行するので後ろのビューが見えるようになります。この特性を活かしてZStack
内でFamilyActivityPicker
の背後にContentUnavailableView
を配置すれば、問題後にユーザーにエラー表示ができます。
ZStack {
ContentUnavailableView(...)
FamilyActivityPicker(...)
.allowsHitTesting(false)
}
このとき、allowsHItTesting(false)
をつけておくと、FamilyActivityPicker
がクラッシュするまでは操作可能を維持しつつ、クラッシュ後=透過後には ContentUnavailableView
のボタンがタップ可能になります。
次なる問題として、クラッシュ時に画面がフリーズすることがあるのですが、何もしないと10秒程度操作を受け付けなくなってしまいます。しかし、強制的にビューをアップデートすることで即時透過状態に移行してくれるため回避可能です。ちょっとHackyですが、毎秒更新を呼ぶタイマー・更新フラグとユーザーは全く触れることのないダミービューを利用することで、強制的にビューアップデートを生み出し迅速なエラー提示を行えます。
struct MyView : View {
let stateUpdateTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State private var updateFlag: Bool = false
var body : some View {
ZStack {
Text(verbatim: "A") // This should not be empty.
.foregroundStyle(.clear)
.accessibilityHidden(true)
.opacity(updateFlag ? 1 : 0)
ContentUnavailableView(...)
FamilyActivityPicker(...)
.allowsHitTesting(false)
}
.onReceive(stateUpdateTimer) { _ in
updateFlag.toggle()
}
}
}
ContentUnavailableView
のアクションでリロード(再読み込み)機能を提供したい場合は、以下のように実装を拡張します。
SwiftUIはView Identityを見て再描画を行うかどうかを決めるので、ここでは.id(familyActivityPickerID)
として設定しているIDをリロードボタンタップ時に別のUUIDに書き換えることで、強制的に再描画を引き起こしています。
struct MyView : View {
let stateUpdateTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State private var updateFlag: Bool = false
@State private var familyActivityPickerID: UUID = .init()
var body : some View {
ZStack {
Text(verbatim: "A") // This should not be empty.
.foregroundStyle(.clear)
.accessibilityHidden(true)
.opacity(updateFlag ? 1 : 0)
ContentUnavailableView {
Label(...)
} actions: {
Button("Reload", systemImage: "arrow.clockwise") {
familyActivityPickerID = UUID()
}
}
FamilyActivityPicker(...)
.allowsHitTesting(false)
.id(familyActivityPickerID)
}
.onReceive(stateUpdateTimer) { _ in
updateFlag.toggle()
}
}
}
ここまでやれば、そもそものカテゴリ開き時のクラッシュはどうにもなっていないものの、その後のケアに関しては出来ることはほぼ全てやったと言えるでしょう。筆者がリリースしたスクリーンタイム制限アプリのAppStopsではこれに加えて、専用ヘルプページの提供を行っています。
おわりに
FamilyActivityPickerの既知の問題点
FamilyActivityPickerは、XPC技術を利用してプライバシーを保護しながらトークンを提供しますが、大量の項目を含むカテゴリを開こうとするとクラッシュやフリーズが発生します。Appleはこの問題を認識しているものの、現在(2025/01/22)のところ修正されていません。筆者が作成したフォーラムポストに多くの開発者のコメントが集まったため、合わせてチェックしてみてください。
クラッシュ時のワークアラウンド
クラッシュ自体を防ぐ方法はありませんが、ContentUnavailableViewを活用してエラーを即座にユーザーにフィードバックする方法はあります。
フィードバック報告
問題の解決を促すためにはフィードバックアシスタントを通じてAppleにフィードバックを報告することが何より重要です。多くの報告が集まるほど修正の優先度が高まると言われています。この記事を読んで同じ悩みを持っている人は、ぜひご自身のフィードバックレポートを投稿してください。
紹介
記事内のワークアラウンドは筆者がリリースしたAppStopsというスクリーンタイム制限アプリで実際に利用されています!よければ実際に動かしながらワークアラウンドの詳細について試してみてください。
Discussion