スクリーンタイムアプリのデバッグ方法
はじめに
iOSでスクリーンタム領域のアプリを作る際は、最低でもDeviceActivityMonitor、多ければShieldConfigurationDataSourceやShieldActionDelegateなどを含めた3つ4つのApp Extensionを駆使しながら実装を行うことになります。
いちおう、Containing AppとApp Extensionを同時にデバッグするで解説されているように、ホストアプリを実行 + App Extensionのプロセスをアタッチでbreakpointが止まるはずなのですが、何故か思い通りにいかないことも多いので、デバッグはOSLog.Loggerを使った方法をお勧めします。
サンプルコードと解説
例えば、以下のようにDeviceActivityMonitorのサブクラスがあり、開始と終了をログで確認したいとします。
import DeviceActivity
class DeviceActivityMonitorExtension: DeviceActivityMonitor {
override func intervalDidStart(for activity: DeviceActivityName) {
super.intervalDidStart(for: activity)
}
override func intervalDidEnd(for activity: DeviceActivityName) {
super.intervalDidEnd(for: activity)
}
}
このとき、Loggerを使って下記のようにログを追加してみます。Loggerはデフォルトではプライバシーのために埋め込まれた変数をログの出力から隠すようになっているので、必要に応じてprivacy: .publicを指定してください。
import DeviceActivity
import OSLog
class DeviceActivityMonitorExtension: DeviceActivityMonitor {
private let logger = Logger(subsystem: "com.domain.projectname", category: "DeviceActivityMonitor")
override func intervalDidStart(for activity: DeviceActivityName) {
logger.notice("Begin \(#function). activity: \(activity.rawValue, privacy: .public)")
super.intervalDidStart(for: activity)
logger.notice("End \(#function). activity: \(activity.rawValue, privacy: .public)")
}
override func intervalDidEnd(for activity: DeviceActivityName) {
logger.notice("Begin \(#function). activity: \(activity.rawValue, privacy: .public)")
super.intervalDidEnd(for: activity)
logger.notice("End \(#function). activity: \(activity.rawValue, privacy: .public)")
}
}
上記のコードでは、各メソッドに入った時/抜ける前にログをしこんでおり、どの DeviceActivityName
によってインターバルが開始/終了されたかをログで知ることができます。
この調子で「エラーが起こるところではlogger.errorを使う」など、自前のログ方針に沿ってログを仕込んでいけば完成です。
Loggerを使ってもXcodeのデバッグエリアにログは表示はされないのですが、Consoleアプリではきっちり表示されます。画像のようにConsole.appを開いて実機のiPhoneかiPadをナビゲーションペインから選択し、中央の「ストリーミングを開始」を押すとログの監視が始まります。監視中だとApp Exntensionでのログも表示されます。
手順としては下記の順番で良いと思います。
- Consoleアプリを立ち上げて実機のストリーミングの開始
- Xcodeから該当アプリを実行し、DeviceActivityMonitorが起動される操作をする
膨大な数のログが表示されるので、ログを初期化するときのsubsystemやcategoryに渡した文字列でフィルタリングするなど、必要なものだけが表示されるように工夫が必要です。いくつか試しましたが、この地道な方法が最もデバッグの効率が良いです。
おすすめデバッグログ方針
デバッグ目的(≠サーバー送信目的ではない)の際のおすすめのApp Extensionでのログ方針は「開始/終了時+メソッドコール前+分岐先」というものです。
import DeviceActivity
import OSLog
class DeviceActivityMonitorExtension: DeviceActivityMonitor {
private let logger = Logger(subsystem: "com.domain.projectname", category: "DeviceActivityMonitor")
private let model = Model()
override func intervalDidStart(for activity: DeviceActivityName) {
logger.notice("Begin \(#function). activity: \(activity.rawValue, privacy: .public)")
super.intervalDidStart(for: activity)
logger.notice("Do important operation")
model.doImportantOperation()
if model.shouldShield {
logger.notice("Shield all applications.")
ManagedSettingsStore().shield.applicationCategories = .all()
} else {
logger.notice("This doesn't shield any applications.")
}
logger.notice("End \(#function). activity: \(activity.rawValue, privacy: .public)")
}
}
色々試してみましたが、これが1番デバッグ中に分かりやすいかなと考えています。もっと良い方針あったらコメントで教えてください。
おわりに
- DeviceActivityMonitor(もしくはApp Extension全般)のデバッグは、breakpointを使うよりもLoggerとConsole.appを組み合わせるのが効率が良く確実に進められる。
- Console.appでは大量に情報が表示されるので、subsystemとcategoryの識別子を使ったフィルタリングは必須。
- おすすめデバッグログ方針は「開始/終了時+メソッドコール前+分岐先」。
紹介
記事内のデバッグ方法を活用して開発したAppStopsというスクリーンタイムアプリをリリースしました!よければ動かしながらスクリーンタイム領域でどんなことができるのか試してみてください。
Discussion