mcpField
F
focus-engine

focus-engine

github.com/dpearson2699開発1.2k2.7k 回インストール13 週間前に更新
リポジトリ

focus-engine について

focus-engineは、SwiftUIおよびUIKitアプリでキーボードや方向キーによるフォーカスを実装する開発スキルです。フォーカス状態、シーン値、復元、プラットフォーム別の移動を扱い、アクセシビリティフォーカスやvisionOSの視線入力とは区別します。

主な機能

  1. 01SwiftUI FocusStateによる管理
  2. 02初期フォーカス先の設定
  3. 03フォーカス値とシーン値の公開
  4. 04フォーカスセクションによる方向移動
  5. 05UIKitフォーカスガイドによる復元と経路指定

活用シーン

  1. 01フォーム項目間のキーボードフォーカス制御
  2. 02macOSやtvOSの不規則なレイアウトの移動
  3. 03コマンドをフォーカス中のシーン内容に連動
  4. 04オーバーレイを閉じた後のフォーカス復元

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill focus-engine

インストール後、Claude Code で/focus-engineを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
references/
focus-debugging.md
focus-patterns.md
multi-platform-focus.md

詳細

フォーカスエンジン

iOS 26 以降、iPadOS、macOS、tvOS、および接続入力経路を使用する visionOS を対象とした SwiftUI と UIKit アプリのフォーカス動作を扱います。キーボードフォーカス、方向キーによるフォーカス、シーンフォーカス値、フォーカスの復元、UIKit フォーカスガイドを取り上げます。このスキルの focusSection() に関するガイダンスは、macOS と tvOS に適用されます。visionOS の視線によるホバーは入力手段であり、フォーカスではありません。VoiceOver と Switch Control に関するアクセシビリティ固有のフォーカスは、ios-accessibility スキルで扱います。

リクエストにフォーカスとアクセシビリティ、または空間入力が混在する場合は、対象範囲を明確に分けてください。

  • キーボード、リモコン、ゲームコントローラー、シーンのフォーカス動作には、このスキルを使用します。
  • visionOS では、視線、ダイレクトタッチ、ポインターによるターゲティングを、フォーカスではなくホバーや入力の手段として説明します。
  • VoiceOver、Switch Control、Voice Control、またはアクセシビリティ要素の順序については、ios-accessibility への簡潔な案内だけを行います。

目次

SwiftUI FocusState

シーン内のフォーカス位置を読み取り、書き込むには @FocusState を使用します。単一の対象には Bool を、複数の対象にはオプショナルな Hashable 列挙型を使用します。

struct LoginView: View {
    enum Field: Hashable { case email, password }

    @State private var email = ""
    @State private var password = ""
    @FocusState private var focusedField: Field?

    var body: some View {
        Form {
            TextField("Email", text: $email)
                .focused($focusedField, equals: .email)

            SecureField("Password", text: $password)
                .focused($focusedField, equals: .password)
        }
        .onAppear { focusedField = .email }
        .onSubmit {
            switch focusedField {
            case .email: focusedField = .password
            case .password, nil: submit()
            }
        }
    }
}

フォーカス状態は、フォーカス可能なコントロールを所有するビュー内に保持します。

デフォルトフォーカス

ビューの表示時、またはフォーカスが自動的に再割り当てされる際に、優先する初期フォーカス領域やコントロールを設定するには .defaultFocus を使用します。

struct SidebarView: View {
    enum Target: Hashable { case library, settings }
    @FocusState private var focusedTarget: Target?

    var body: some View {
        VStack {
            Button("Library") { }
                .focused($focusedTarget, equals: .library)

            Button("Settings") { }
                .focused($focusedTarget, equals: .settings)
        }
        .defaultFocus($focusedTarget, .library)
    }
}

画面またはフォーカス領域ごとに、明確なデフォルトの移動先を 1 つ指定してください。

フォーカス値とシーン値

現在フォーカスされているビューの状態を公開するには、フォーカス値を使用します。コマンドやシーン全体の UI が、シーン内でフォーカスが移動した後も値にアクセスできるようにするには、シーンフォーカス値を使用します。

struct SelectedRecipeKey: FocusedValueKey {
    typealias Value = Binding<Recipe>
}

extension FocusedValues {
    var selectedRecipe: Binding<Recipe>? {
        get { self[SelectedRecipeKey.self] }
        set { self[SelectedRecipeKey.self] = newValue }
    }
}

struct RecipeDetailView: View {
    @Binding var recipe: Recipe

    var body: some View {
        Text(recipe.title)
            .focusedSceneValue(\.selectedRecipe, $recipe)
    }
}

フォーカスされているシーンの現在のコンテンツに作用する必要があるメニュー、コマンド、ツールバーには、このパターンを使用します。

フォーカス可能な操作

キーボードや方向キーによるフォーカスの対象にするカスタム SwiftUI ビューには、.focusable(_:interactions:) を使用します。

struct SelectableCard: View {
    let title: String
    let action: () -> Void
    @FocusState private var isFocused: Bool

    var body: some View {
        Button(action: action) {
            RoundedRectangle(cornerRadius: 12)
                .fill(isFocused ? Color.accentColor.opacity(0.15) : .clear)
                .overlay { Text(title) }
        }
        .buttonStyle(.plain)
        .focusable(interactions: .activate)
        .focused($isFocused)
    }
}

任意のジェスチャー駆動型ビューをフォーカス可能にする前に、セマンティックな Button、Toggle、TextField などのシステムコントロールを優先してください。カスタムのボタン風コントロールには、セマンティックなコントロールで UI を表現できない場合に限り、.focusable(interactions: .activate) を使用します。より幅広いインタラクションは、編集や複数のフォーカス駆動動作が本当に必要なビューに限定してください。

フォーカスセクション

不均一なレイアウトでフォーカス可能な子孫ビューのグループ間の方向キー移動を案内するには、macOS 13 以降および tvOS 15 以降で focusSection() を使用します。

struct TVLibraryView: View {
    var body: some View {
        HStack {
            VStack {
                Button("Recent") { }
                Button("Favorites") { }
                Button("Downloaded") { }
            }
            .focusSection()

            VStack {
                Button("Featured") { }
                Button("Top Picks") { }
                Button("Continue Watching") { }
            }
            .focusSection()
        }
    }
}

デフォルトの左右または上下の移動で意図したグループがスキップされる場合は、macOS と tvOS でフォーカスセクションを使用します。

フォーカスの復元

シート、ポップオーバー、一時的なオーバーレイを閉じた後は、安定した起点または論理的な次の移動先にフォーカスを戻します。

struct FiltersView: View {
    @State private var showSheet = false
    @FocusState private var isFilterButtonFocused: Bool

    var body: some View {
        Button("Filters") { showSheet = true }
            .focused($isFilterButtonFocused)
            .sheet(isPresented: $showSheet) {
                FilterEditor()
                    .onDisappear {
                        Task { @MainActor in
                            isFilterButtonFocused = true
                        }
                    }
            }
    }
}

表示状態の変化によってユーザーが戸惑う可能性がある場合は、必ず意図的にフォーカスを復元してください。

UIKit フォーカスガイド

空白領域や扱いにくいジオメトリをまたいで移動先をカスタマイズする必要がある UIKit または tvOS のレイアウトでは、UIFocusGuide を使用します。

final class DashboardViewController: UIViewController {
    private let focusGuide = UIFocusGuide()
    @IBOutlet private weak var leadingButton: UIButton!
    @IBOutlet private weak var trailingButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addLayoutGuide(focusGuide)
        focusGuide.preferredFocusEnvironments = [trailingButton]

        NSLayoutConstraint.activate([
            focusGuide.leadingAnchor.constraint(equalTo: leadingButton.trailingAnchor),
            focusGuide.trailingAnchor.constraint(equalTo: trailingButton.leadingAnchor),
            focusGuide.topAnchor.constraint(equalTo: leadingButton.topAnchor),
            focusGuide.bottomAnchor.constraint(equalTo: leadingButton.bottomAnchor)
        ])
    }
}

UIFocusGuide は不可視であり、ビューではありません。装飾的な UI を追加せずにフォーカスを別の場所へ誘導するために使用します。

よくある間違い

  1. アクセシビリティフォーカスとキーボードまたは方向キーによるフォーカスを、同じ考え方で扱う。
  2. @FocusState を所有するビューではなく、共有モデルに保存する。
  3. 1つの画面に、競合するデフォルトのフォーカスターゲットを複数設定する。
  4. 装飾用ビューに .focusable() を使用する。
  5. シート、ポップオーバー、カスタムオーバーレイの後にフォーカスを復元し忘れる。
  6. macOS または tvOS で focusSection() を試す前や、SwiftUI でより適切なレイアウトグループ化を試す前に、UIFocusGuide を使おうとする。
  7. 可能な場合にセマンティックな Button を使わず、カスタムのフォーカス可能なコントロールの主要なアクションにジェスチャーハンドラーを使用する。
  8. visionOS の視線によるホバーをフォーカスとして扱う。キーボードやゲームコントローラーなど、接続された入力デバイスにはフォーカスガイダンスを使用してください。

レビューチェックリスト

  • @FocusState は、コントロールを所有するビューにローカルに配置されている
  • 画面に初期フォーカスが必要な場合、ターゲットが明示されている
  • フィールドまたはグループ間のフォーカス移動が決定的である
  • コマンドで現在のシーン状態が必要な場合、focusedSceneValue または関連するフォーカス値 API が使用されている
  • カスタムコントロールは、本当にインタラクティブな場合にのみフォーカスを受け取る
  • macOS または tvOS の不均一な方向キー操作レイアウトでは、UIKit に移行する前に focusSection() が使用されている
  • 一時的な表示が閉じた後、フォーカスが安定した要素に戻る
  • UIFocusGuide のジオメトリと優先移動先が、意図した経路に一致している
  • visionOS のガイダンスで、接続デバイスによるフォーカスと、視線によるホバーまたは RealityKit の入力ターゲットが区別されている
  • アクセシビリティフォーカスに関する懸念は ios-accessibility で扱い、キーボードや方向キーによるフォーカスのロジックと混在させていない

参考資料

コメント