mcpField
A
axiom-deep-link-debugging

axiom-deep-link-debugging

github.com/charleswiltgenテスト・品質1.2k170 回インストール今日更新
リポジトリ

axiom-deep-link-debugging について

iOS 開発者向けに、デバッグ用ディープリンクでシミュレーター上の画面や状態へ直接移動する方法を解説します。SwiftUI や NavigationStack との連携、パラメーター検証、デバッグ用機能をリリースに含めない方法を扱います。

主な機能

  1. 01デバッグ専用 URL スキーム
  2. 02パラメーター付き画面遷移
  3. 03アプリ状態を設定するリンク
  4. 04既存ナビゲーションとの連携
  5. 05リリースへのデバッグコード混入防止

活用シーン

  1. 01スクリーンショット用の画面へ直接移動する
  2. 02手動操作なしでエラーや空状態をテストする
  3. 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 エージェントで動作する

実例

シナリオ:エディター画面でレシピアプリのレイアウトの問題をデバッグしています。

導入前(手動テスト):

  1. アプリをビルド → 30 秒
  2. シミュレーターを起動
  3. 「Recipes」をタップ → 読み込みを待つ
  4. レシピ #42 までスクロール
  5. タップして詳細を開く
  6. 「Edit」をタップ
  7. レイアウトが修正されたか確認
  8. 変更を加えて再ビルド → ステップ 1 から繰り返す 合計:1 回の反復につき 2~3 分

導入後(デバッグ用ディープリンクを使用):

  1. アプリをビルド → 30 秒
  2. 実行:xcrun simctl openurl booted "debug://recipe-edit?id=42"
  3. 実行:/axiom:screenshot
  4. Claude がスクリーンショットを分析し、レイアウトの修正を確認
  5. 必要に応じて変更を加えて再ビルド → ステップ 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%高速化
  • 手動でナビゲーションすることなく自動テスト
  • あらゆるアプリ状態のスクリーンショット自動化

以下を忘れないでください:

  1. すべてのデバッグコードを #if DEBUG で囲む
  2. リリースビルドから URL スキームを削除する
  3. 既存のナビゲーションと統合し、ロジックを重複させない
  4. すべてのパラメーターを検証する(強制アンラップしない)
  5. チームメンバー向けに記載する

コメント