axiom-deep-link-debugging について
iOS 開発者向けに、デバッグ用ディープリンクでシミュレーター上の画面や状態へ直接移動する方法を解説します。SwiftUI や NavigationStack との連携、パラメーター検証、デバッグ用機能をリリースに含めない方法を扱います。
主な機能
- 01デバッグ専用 URL スキーム
- 02パラメーター付き画面遷移
- 03アプリ状態を設定するリンク
- 04既存ナビゲーションとの連携
- 05リリースへのデバッグコード混入防止
活用シーン
- 01スクリーンショット用の画面へ直接移動する
- 02手動操作なしでエラーや空状態をテストする
- 03UI 修正後の検証時間を短縮する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-deep-link-debuggingインストール後、Claude Code で/axiom-deep-link-debuggingを入力して読み込まれたか確認します。
詳細
ディープリンクのデバッグ
このスキルを使う場面
以下の場合に使用します:
- シミュレーターでのテスト用に、デバッグ専用のディープリンクを追加する
- スクリーンショットの撮影やテストのために、特定の画面へ自動で移動できるようにする
simulator-testerエージェントまたは/axiom:screenshotと連携する- 本番用のディープリンク実装をせずに、プログラムから画面を移動する必要がある
- 手動でタップせずに画面遷移フローをテストする
以下の場合は使用しないでください:
- 本番用のディープリンク(代わりに
axiom-swiftui-navスキルを使用してください) - ユニバーサルリンクまたは App Clips
- 複雑なルーティングアーキテクチャ
プロンプトの例
1. 「Claude Code でテスト対象の画面へ移動できない」
→ xcrun simctl openurl による画面遷移を可能にする、デバッグ専用の URL スキームを追加します
2. 「さまざまな画面のスクリーンショットを自動で撮影したい」
→ シミュレーターから呼び出せる、各画面用のデバッグディープリンクを作成します
3. 「自動テストで特定のアプリ状態を設定する必要がある」
→ 画面遷移と状態設定を行うデバッグリンクを追加します
要注意の兆候 — デバッグディープリンクが必要な場合
以下のいずれかに当てはまる場合は、デバッグディープリンクを追加してください:
テストの手間:
- ❌ 「この機能をテストするのに、5 画面も手動でタップして進まなければならない」
- ❌ 「デバッグしたい画面をスクリーンショットで表示できない」
- ❌ 「複雑なセットアップをしないと、自動テストでエラー状態に到達できない」
デバッグの非効率さ:
- ❌ 「修正してビルドし、手動で画面を移動して確認するまでに、1 回の反復で 3 分かかる」
- ❌ 「Claude Code で該当画面へ移動できず、修正を目視確認できない」
解決策:デバッグディープリンクを追加して、任意の状態設定で任意の画面へ直接移動できるようにします。これにより、あなたも Claude Code も画面へ直接移動できます。
実装
パターン 1:基本的なデバッグ URL スキーム(SwiftUI)
画面へルーティングする、デバッグ専用の URL スキームを追加します。
import SwiftUI
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
#if DEBUG
.onOpenURL { url in
handleDebugURL(url)
}
#endif
}
}
#if DEBUG
private func handleDebugURL(_ url: URL) {
guard url.scheme == "debug" else { return }
// Route based on host
switch url.host {
case "settings":
// Navigate to settings
NotificationCenter.default.post(
name: .navigateToSettings,
object: nil
)
case "profile":
// Navigate to profile
let userID = url.queryItems?["id"] ?? "current"
NotificationCenter.default.post(
name: .navigateToProfile,
object: userID
)
case "reset":
// Reset app to initial state
resetApp()
default:
print("⚠️ Unknown debug URL: \(url)")
}
}
#endif
}
#if DEBUG
extension Notification.Name {
static let navigateToSettings = Notification.Name("navigateToSettings")
static let navigateToProfile = Notification.Name("navigateToProfile")
}
extension URL {
var queryItems: [String: String]? {
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false),
let items = components.queryItems else {
return nil
}
return Dictionary(uniqueKeysWithValues: items.map { ($0.name, $0.value ?? "") })
}
}
#endif
使用方法:
# From simulator
xcrun simctl openurl booted "debug://settings"
xcrun simctl openurl booted "debug://profile?id=123"
xcrun simctl openurl booted "debug://reset"
パターン 2:NavigationPath との統合(iOS 16 以降)
堅牢なナビゲーションを実現するため、デバッグディープリンクを NavigationStack と統合します。
import SwiftUI
@MainActor
class DebugRouter: ObservableObject {
@Published var path = NavigationPath()
#if DEBUG
func handleDebugURL(_ url: URL) {
guard url.scheme == "debug" else { return }
switch url.host {
case "settings":
path.append(Destination.settings)
case "recipe":
if let id = url.queryItems?["id"], let recipeID = Int(id) {
path.append(Destination.recipe(id: recipeID))
}
case "recipe-edit":
if let id = url.queryItems?["id"], let recipeID = Int(id) {
// Navigate to recipe, then to edit
path.append(Destination.recipe(id: recipeID))
path.append(Destination.recipeEdit(id: recipeID))
}
case "reset":
path = NavigationPath() // Pop to root
default:
print("⚠️ Unknown debug URL: \(url)")
}
}
#endif
}
struct ContentView: View {
@StateObject private var router = DebugRouter()
var body: some View {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: Destination.self) { destination in
destinationView(for: destination)
}
}
#if DEBUG
.onOpenURL { url in
router.handleDebugURL(url)
}
#endif
}
@ViewBuilder
private func destinationView(for destination: Destination) -> some View {
switch destination {
case .settings:
SettingsView()
case .recipe(let id):
RecipeDetailView(recipeID: id)
case .recipeEdit(let id):
RecipeEditView(recipeID: id)
}
}
}
enum Destination: Hashable {
case settings
case recipe(id: Int)
case recipeEdit(id: Int)
}
使用方法:
# Navigate to settings
xcrun simctl openurl booted "debug://settings"
# Navigate to recipe #42
xcrun simctl openurl booted "debug://recipe?id=42"
# Navigate to recipe #42 edit screen
xcrun simctl openurl booted "debug://recipe-edit?id=42"
# Pop to root
xcrun simctl openurl booted "debug://reset"
パターン 3:状態設定リンク
画面遷移と状態設定の両方を行うデバッグリンクです。
#if DEBUG
extension DebugRouter {
func handleDebugURL(_ url: URL) {
guard url.scheme == "debug" else { return }
switch url.host {
case "login":
// Show login screen
path.append(Destination.login)
case "login-error":
// Show login screen WITH error state
path.append(Destination.login)
// Trigger error state
NotificationCenter.default.post(
name: .showLoginError,
object: "Invalid credentials"
)
case "recipe-empty":
// Show recipe list in empty state
UserDefaults.standard.set(true, forKey: "debug_emptyRecipeList")
path.append(Destination.recipes)
case "recipe-error":
// Show recipe list with network error
UserDefaults.standard.set(true, forKey: "debug_networkError")
path.append(Destination.recipes)
default:
print("⚠️ Unknown debug URL: \(url)")
}
}
}
#endif
使用方法:
# Test login error state
xcrun simctl openurl booted "debug://login-error"
# Test empty recipe list
xcrun simctl openurl booted "debug://recipe-empty"
# Test network error handling
xcrun simctl openurl booted "debug://recipe-error"
パターン 4:Info.plist の設定(DEBUG のみ)
デバッグビルドでのみ、デバッグ URL スキームを登録します。
手順 1:Info.plist にスキームを追加する
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>debug</string>
</array>
<key>CFBundleURLName</key>
<string>com.example.debug</string>
</dict>
</array>
手順 2:リリースビルドから削除する
ターゲットの Build Phases に Run Script フェーズを追加します(「Copy Bundle Resources」の前に実行されます):
# Strip debug URL scheme from Release builds
if [ "${CONFIGURATION}" = "Release" ]; then
echo "Removing debug URL scheme from Info.plist"
/usr/libexec/PlistBuddy -c "Delete :CFBundleURLTypes:0" "${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}" 2>/dev/null || true
fi
別の方法:Build Settings で Debug と Release の構成ごとに、個別の Info.plist ファイルを使用します。
シミュレーターでのテストとの統合
/axiom:screenshot コマンドとの連携
# 1. Navigate to screen
xcrun simctl openurl booted "debug://settings"
# 2. Wait for navigation
sleep 1
# 3. Capture screenshot
/axiom:screenshot
simulator-tester エージェントとの連携
エージェントに次のように伝えてください:
- 「Settings に移動してスクリーンショットを撮って」
- 「レシピエディターを開いて、レイアウトを確認して」
- 「エラー状態に移動して、どのように見えるか教えて」
エージェントはデバッグディープリンクを使って画面を移動します。
必ず最初に行う手順
デバッグディープリンクを追加する前に、必ず次の手順を完了してください:
手順 1:ナビゲーションの要件を定義する
テストで移動する必要がある画面をすべて列挙します:
- Settings screen
- Profile screen (with specific user ID)
- Recipe detail (with specific recipe ID)
- Error states (login error, network error, etc.)
- Empty states (no recipes, no favorites)
手順 2:URL スキームのパターンを選ぶ
debug://screen-name # Simple screen navigation
debug://screen-name?param=value # Navigation with parameters
debug://state-name # State configuration
手順 3:URL ハンドラーを追加する
#if DEBUG を使用して、リリースビルドからコードが除外されるようにします。
手順 4:ディープリンクをテストする
# Boot simulator
xcrun simctl boot "iPhone 16 Pro"
# Launch app
xcrun simctl launch booted com.example.YourApp
# Test each deep link
xcrun simctl openurl booted "debug://settings"
xcrun simctl openurl booted "debug://profile?id=123"
よくある間違い
❌ 誤り — URL ハンドラーにナビゲーションを直接記述する
#if DEBUG
func handleDebugURL(_ url: URL) {
if url.host == "settings" {
// ❌ WRONG — Creates tight coupling
self.showingSettings = true
}
}
#endif
問題点:URL ハンドラーがナビゲーションロジックを持つことになり、コーディネーターやルーターのパターンと重複します。
✅ 正しい方法 — 既存のナビゲーションシステムを使う:
#if DEBUG
func handleDebugURL(_ url: URL) {
if url.host == "settings" {
// Use existing NavigationPath
path.append(Destination.settings)
}
}
#endif
❌ 誤り — デバッグコードを本番環境に残す
// ❌ WRONG — No #if DEBUG
func handleDebugURL(_ url: URL) {
// This ships to users!
}
問題点:本番環境でデバッグ用エンドポイントが公開され、セキュリティリスクになります。
✅ 正しい方法 — #if DEBUG で囲む:
#if DEBUG
func handleDebugURL(_ url: URL) {
// Stripped from release builds
}
#endif
❌ 誤り — 検証せずにクエリパラメーターを使う
#if DEBUG
case "profile":
let userID = Int(url.queryItems?["id"] ?? "0")! // ❌ Force unwrap
path.append(Destination.profile(id: userID))
#endif
問題点:id が欠落しているか無効な場合、クラッシュします。
✅ 正しい方法 — パラメーターを検証する:
#if DEBUG
case "profile":
guard let idString = url.queryItems?["id"],
let userID = Int(idString) else {
print("⚠️ Invalid profile ID")
return
}
path.append(Destination.profile(id: userID))
#endif
テストチェックリスト
自動ワークフローでデバッグ用ディープリンクを使用する前に、以下を確認してください。
- URL ハンドラーが
#if DEBUGで囲まれている - すべてのディープリンクをシミュレーターで手動テストしている
- パラメーターが検証されている(強制アンラップしない)
- ディープリンクが既存のナビゲーションと連携している(ロジックを重複させない)
- URL スキームが Release ビルドから削除されている(スクリプトまたは個別の Info.plist)
- 他の開発者向けに README またはコメントに記載されている
-
/axiom:screenshotコマンドで動作する -
simulator-testerエージェントで動作する
実例
シナリオ:エディター画面でレシピアプリのレイアウトの問題をデバッグしています。
導入前(手動テスト):
- アプリをビルド → 30 秒
- シミュレーターを起動
- 「Recipes」をタップ → 読み込みを待つ
- レシピ #42 までスクロール
- タップして詳細を開く
- 「Edit」をタップ
- レイアウトが修正されたか確認
- 変更を加えて再ビルド → ステップ 1 から繰り返す 合計:1 回の反復につき 2~3 分
導入後(デバッグ用ディープリンクを使用):
- アプリをビルド → 30 秒
- 実行:
xcrun simctl openurl booted "debug://recipe-edit?id=42" - 実行:
/axiom:screenshot - Claude がスクリーンショットを分析し、レイアウトの修正を確認
- 必要に応じて変更を加えて再ビルド → ステップ 2 から繰り返す 合計:1 回の反復につき 45 秒
時間の節約:視覚的な検証を行いながら、反復を 60~75%高速化
既存のナビゲーションとの統合
NavigationStack を使用するアプリの場合
既存の NavigationPath に追加するデバッグ用 URL ハンドラーを追加します。
router.path.append(Destination.fromDebugURL(url))
Coordinator パターンを使用するアプリの場合
デバッグ用 URL ハンドラーから Coordinator のメソッドを呼び出します。
coordinator.navigate(to: .fromDebugURL(url))
カスタムルーティングを使用するアプリの場合
ルーターのナビゲーション API と統合します。
AppRouter.shared.push(Screen.fromDebugURL(url))
重要な原則:デバッグ用ディープリンクでは、既存のナビゲーションを置き換えるのではなく、既存のナビゲーションを使用してください。
高度なパターン
パターン 5:パラメーター化された状態の設定
#if DEBUG
case "test-scenario":
// Parse complex test scenario from URL
// Example: debug://test-scenario?user=premium&recipes=empty&network=slow
if let userType = url.queryItems?["user"] {
configureUser(type: userType) // "premium", "free", "trial"
}
if let recipesState = url.queryItems?["recipes"] {
configureRecipes(state: recipesState) // "empty", "full", "error"
}
if let networkState = url.queryItems?["network"] {
configureNetwork(state: networkState) // "fast", "slow", "offline"
}
// Now navigate
path.append(Destination.recipes)
#endif
使用方法:
# Test premium user with empty recipe list
xcrun simctl openurl booted "debug://test-scenario?user=premium&recipes=empty"
# Test slow network with error handling
xcrun simctl openurl booted "debug://test-scenario?network=slow&recipes=error"
パターン 6:スクリーンショット自動化ヘルパー
状態を設定し、スクリーンショットを撮影する単一の URL を作成します。
#if DEBUG
case "screenshot":
// Parse screen and configuration
guard let screen = url.queryItems?["screen"] else { return }
// Configure state
if let state = url.queryItems?["state"] {
applyState(state)
}
// Navigate
navigate(to: screen)
// Post notification for external capture
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
NotificationCenter.default.post(
name: .readyForScreenshot,
object: screen
)
}
#endif
使用方法:
# Navigate to login screen with error state, wait, then screenshot
xcrun simctl openurl booted "debug://screenshot?screen=login&state=error"
sleep 2
xcrun simctl io booted screenshot login-error.png
関連スキル
axiom-swiftui-nav— 本番環境向けのディープリンクと NavigationStack のパターンsimulator-tester— デバッグ用ディープリンクを使用したシミュレーターの自動テストaxiom-xcode-debugging— 環境を優先するデバッグワークフロー
まとめ
デバッグ用ディープリンクを使うと、以下が可能になります。
- 視覚的な検証を伴うクローズドループデバッグ
- 視覚的な修正における反復を 60~75%高速化
- 手動でナビゲーションすることなく自動テスト
- あらゆるアプリ状態のスクリーンショット自動化
以下を忘れないでください:
- すべてのデバッグコードを
#if DEBUGで囲む - リリースビルドから URL スキームを削除する
- 既存のナビゲーションと統合し、ロジックを重複させない
- すべてのパラメーターを検証する(強制アンラップしない)
- チームメンバー向けに記載する