WWDC Quick Look 💓 By SwiftGGTeam
The SwiftUI cookbook for focus

The SwiftUI cookbook for focus

元の動画を見る

ハイライト

SwiftUI は、iOS 17、macOS Sonoma、tvOS 17、watchOS 10 の Focus システムを新しい機能で拡張します。.focusable(interactions:)フォーカスインタラクションタイプの正確な制御、@FocusStateカスタムデータタイプをサポートし、focusedSceneValueクロスビューデータフローを実装し、focusSection()ガイド付き方向パッド ナビゲーション、およびonKeyPressそしてdefaultFocusキーボード駆動のインタラクティブなエクスペリエンスを正確にプログラム可能にします。

主な内容

フォーカスはアテンション カーソルです

キーボード、Apple TV リモコン、Apple Watch Digital Crown には共通点が 1 つあります。それは、画面座標を提供しないことです。マウスをクリックすると、システムはカーソルがどこにあるかを認識します。ただし、キーボードのキーが押されたとき、システムは入力をどのビューに送信する必要があるかを認識する必要があります。

01:05

フォーカスは、この問題を解決するための一連のメカニズムです。これは、入力を受け取る現在のビューをマークする特別な「アテンション カーソル」のように機能します。 macOS はフォーカス ビューに境界線を追加し、watchOS は緑色の境界線を描画し、tvOS はフォーカス ビューにフローティング効果を追加します。

02:17

2 つのフォーカス インタラクション タイプ

iOS17以前では、.focusable行為は一つしかない。これで、インタラクション タイプを正確に指定できるようになりました。

  • .focusable(interactions: .edit): テキスト ボックスなど、継続的なフォーカス入力が必要なコントロールに使用例されます。クリックしてフォーカスを取得します。 -.focusable(interactions: .activate): ボタンなど、クリックの代わりにフォーカスを使用例するコントロールに使用例されます。 Tab キーは、キーボード ナビゲーションがオンになっている場合にのみフォーカスを取得できます。

05:04

以前に macOS で使用例したことがある場合.focusableデフォルトの動作が変更されている可能性があるため、Sonoma にアップグレードした後に動作が期待どおりであるかどうかを確認する必要があります。

FocusState はフォーカス位置を管理します

@FocusStateフォーカス位置を観察し、プログラムで制御できます。 Bool 型は単一のビューに使用例され、カスタム Hashable 型は複数のビューでフォーカスを管理できます。

06:12

価値観を重視 リモート UI に接続

メニュー コマンドやツールバー ボタンなどの UI 要素はメイン コンテンツ領域から離れた場所に配置されますが、現在のフォーカス コンテキストを認識する必要があります。Focused Values定義上FocusedValueKeyにより、フォーカスの移動に応じてデータがリモート UI に自動的に更新されるようになります。

07:32

フォーカス セクションはナビゲーション パスをガイドします

複雑なレイアウトでは、矢印キーまたは Tab キーのフォーカス移動が直感的ではない場合があります。focusSection()ビューのグループをナビゲーション領域としてマークして、焦点を正しい方向に導くことができます。

10:03

##詳細

カスタム コントロールのインタラクションに焦点を当てる

struct RecipeGrid: View {
    var body: some View {
        LazyVGrid(columns: [GridItem(), GridItem()]) {
            ForEach(0..<4) { _ in Capsule() }
        }
        .focusable(interactions: .edit)
    }
}

struct RatingPicker: View {
    var body: some View {
        HStack { Capsule() ; Capsule() }
            .focusable(interactions: .activate)
    }
}

05:05

キーポイント:

  • .focusable(interactions: .edit)クリック時にコントロールにフォーカスを取得します。テキスト入力コントロールに適しています。 -.focusable(interactions: .activate)キーボード ナビゲーションが有効な場合、コントロールが Tab 経由でのみフォーカスを取得できるようにします。ボタン コントロールに適しています。
  • パラメーターが指定されていない場合、コントロールはすべてのインタラクション タイプのフォーカスをサポートします。
  • すでに macOS 上にあります.focusableSonoma にアップグレードするとコードの動作が変わる可能性があるため、確認する必要があります。

FocusState を使用例してフォーカスを観察および制御する

struct GroceryListView: View {
    @FocusState private var isItemFocused
    @State private var itemName = ""

    var body: some View {
        TextField("Item Name", text: $itemName)
            .focused($isItemFocused)

        Button("Done") { isItemFocused = false }
            .disabled(!isItemFocused)
    }
}

06:12

キーポイント:

  • @FocusStateフォーカス位置にバインドされたプロパティを宣言する -.focused($isItemFocused)TextField を FocusState にバインドする
  • 「完了」ボタンをクリックしたときに設定されますisItemFocused = false、フォーカスがテキスト ボックスから移動します。 -ボタンdisabledフォーカス状態に応じてステータスが動的に変化する

Focused Values はメニューコマンドとメインコンテンツの連携を実現します

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

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

struct RecipeView: View {
    @Binding var recipe: Recipe

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

struct RecipeCommands: Commands {
    @FocusedBinding(\.selectedRecipe) private var selectedRecipe: Recipe?

    var body: some Commands {
        CommandMenu("Recipe") {
            Button("Add to Grocery List") {
                if let selectedRecipe {
                    addRecipe(selectedRecipe)
                }
            }
            .disabled(selectedRecipe == nil)
        }
    }

    private func addRecipe(_ recipe: Recipe) { /* ... */ }
}

07:32

キーポイント:

  • 意味FocusedValueKey値のタイプを指定するプロトコル
  • 拡張子FocusedValuesタイプセーフなアクセス プロパティを提供する -.focusedSceneValue(\.selectedRecipe, $recipe)現在のビューにフォーカスがある間にデータを提供します -@FocusedBindingメニューコマンドでフォーカスコンテキストデータを読み取る
  • フォーカスが移動するとメニューコマンドが自動的に更新されます

フォーカス セクション 矢印キーのナビゲーションを改善

struct ContentView: View {
    @State private var favorites = Recipe.examples
    @State private var selection = Recipe.examples.first!

    var body: some View {
        VStack {
            HStack {
                ForEach(favorites) { recipe in
                    Button(recipe.name) { selection = recipe }
                }
            }

            Image(selection.imageName)

            HStack {
                Spacer()
                Button("Add to Grocery List") { addIngredients(selection) }
                Spacer()
            }
            .focusSection()
        }
    }

    private func addIngredients(_ recipe: Recipe) { /* ... */ }
}

10:03

キーポイント:

  • .focusSection()エリアをフォーカス ナビゲーションのターゲットとしてマークする
  • フォーカス セクション自体はフォーカスを取得しませんが、フォーカスが最も近いフォーカスされたコンテンツに移動するようにガイドします。
  • フォーカス セクションを有効にするには、そのサイズが内部コンテンツより大きくなければなりません (Spacer延長)
  • tvOS では、グリッド内の行から左にスワイプしてもサイドバーに到達できない問題が修正されます。

フォーカスの移動をプログラムで制御する

struct GroceryListView: View {
    @State private var list = GroceryList.examples
    @FocusState private var focusedItem: GroceryList.Item.ID?

    var body: some View {
        NavigationStack {
            List($list.items) { $item in
                HStack {
                    Toggle("Obtained", isOn: $item.isObtained)
                    TextField("Item Name", text: $item.name)
                        .onSubmit { addEmptyItem() }
                        .focused($focusedItem, equals: item.id)
                }
            }
            .defaultFocus($focusedItem, list.items.last?.id)
            .toggleStyle(.checklist)
        }
        .toolbar {
            Button(action: addEmptyItem) {
                Label("New Item", systemImage: "plus")
            }
        }
    }

    private func addEmptyItem() {
        let newItem = list.addItem()
        focusedItem = newItem.id
    }
}

11:29

キーポイント:

  • @FocusStateカスタムを使用例するHashableタイプ(ここにあります)Item.ID) 複数のビューでフォーカスを管理します -.focused($focusedItem, equals: item.id)各 TextField を対応する ID にバインドします -.defaultFocus($focusedItem, list.items.last?.id)画面が最初に表示されたときに、フォーカスを最後の項目に自動的に設定します -addEmptyItem()新しいプロジェクトを作成したらすぐに更新するfocusedItem、フォーカスは自動的に新しいテキスト ボックスに移動します。

カスタム フォーカス コントロールはキーボードとデジタル クラウンをサポートします

struct RatingPicker: View {
    @Environment(\.layoutDirection) private var layoutDirection
    @Binding var rating: Rating?

    var body: some View {
        EmojiContainer { ratingOptions }
            .contentShape(.capsule)
            .focusable(interactions: .activate)
            #if os(macOS)
            .onMoveCommand { direction in
                selectRating(direction, layoutDirection: layoutDirection)
            }
            #endif
            #if os(watchOS)
            .digitalCrownRotation($digitalCrownRotation, from: 0, through: Double(Rating.allCases.count - 1), by: 1, sensitivity: .low)
            .onChange(of: digitalCrownRotation) { oldValue, newValue in
                if let rating = Rating(rawValue: Int(round(digitalCrownRotation))) {
                    self.rating = rating
                }
            }
            #endif
    }

    private var ratingOptions: some View {
        ForEach(Rating.allCases) { rating in
            EmojiView(rating: rating, isSelected: self.rating == rating) {
                self.rating = rating
            }
        }
    }
}

15:25

キーポイント:

  • .contentShape(.capsule)フォーカス リングをデフォルトの長方形ではなくカプセルの形状に従うようにします -.focusable(interactions: .activate)アクティベーション タイプとして指定されているため、クリックしてもフォーカスが取得されません -.onMoveCommand矢印キー入力を処理し、macOS で左右の矢印でオプションを切り替えます -.digitalCrownRotationwatchOS でデジタル クラウンの回転をバインドする
  • 使用例するlayoutDirection環境値は右から左への言語のレイアウト反転を処理します

フォーカス可能なグリッドはキーボード ナビゲーションをサポートします

struct ContentView: View {
    @State private var recipes = Recipe.examples
    @State private var selection: Recipe.ID = Recipe.examples.first!.id
    @Environment(\.layoutDirection) private var layoutDirection

    var body: some View {
        LazyVGrid(columns: columns) {
            ForEach(recipes) { recipe in
                RecipeTile(recipe: recipe, isSelected: recipe.id == selection)
                    .id(recipe.id)
                    #if os(macOS)
                    .onTapGesture { selection = recipe.id }
                    .simultaneousGesture(TapGesture(count: 2).onEnded {
                        navigateToRecipe(id: recipe.id)
                    })
                    #else
                    .onTapGesture { navigateToRecipe(id: recipe.id) }
                    #endif
            }
        }
        .focusable()
        .focusEffectDisabled()
        .focusedValue(\.selectedRecipe, $selection)
        .onMoveCommand { direction in
            selectRecipe(direction, layoutDirection: layoutDirection)
        }
        .onKeyPress(.return) {
            navigateToRecipe(id: selection)
            return .handled
        }
        .onKeyPress(characters: .alphanumerics, phases: .down) { keyPress in
            selectRecipe(matching: keyPress.characters)
        }
    }
}

18:50

キーポイント:

  • .focusable()インタラクション タイプが指定されていない場合は、グリッド全体にフォーカスを与え、すべてのインタラクションをサポートします。 -.focusEffectDisabled()選択された状態ですでにフォーカス情報が伝達されているため、自動フォーカス リングをオフにします -.focusedValue(\.selectedRecipe, $selection)現在の選択範囲をメニュー コマンドに公開します -.onMoveCommand矢印キーを処理し、選択した項目を更新します -.onKeyPress(.return)Enter キーを処理し、詳細を入力します -.onKeyPress(characters: .alphanumerics)頭文字の素早い位置決めを実現

重要なポイント

1.完全なキーボード ナビゲーションを macOS アプリケーションに追加

アプリが macOS で実行されている場合は、システム設定で「キーボード ナビゲーション」スイッチをオンにし、Tab キーと矢印キーを使用例してすべての操作を完了できることをテストします。カスタム コントロールを追加する.focusable(interactions: .activate)、コンテナに追加します.focusSection()ガイドナビゲーションパス。入り口は、システム設定 > キーボード > キーボード ナビゲーションです。

2.フォーカスされた値を使用例してメニュー コマンドを簡素化

メニュー コマンド (「お気に入りに追加」、「削除」など) は、現在のフォーカス コンテキストに基づいて使用例できる必要があります。使用例FocusedValueKey + @FocusedBinding選択した状態を渡すことを手動でオーバーライドします。入り口は、FocusedValueKey、メインビューで使用例します.focusedSceneValueデータを提供してくださいCommands中国語での使用例@FocusedBinding読む。

3.食料品リストスタイルの自動フォーカスジャンプを実装します

フォームまたはリストで、ユーザーが新しい項目を追加すると、フォーカスは自動的に新しい入力ボックスに移動します。使用例@FocusState新しいプロジェクトを作成した後、プロジェクト ID をバインドし、バインド値を更新します。入り口は各入力ボックスに追加します.focused($focusedItem, equals: item.id)、addメソッドで設定focusedItem = newItem.id

4. tvOS アプリケーションにフォーカスエリアを追加

tvOS では、リモコンの矢印キーを使用例してフォーカスを移動します。サイドバーとメインコンテンツの間に直接のナビゲーションがない場合は、.focusSection()。入り口は、クロスエリアナビゲーションが必要なレイアウトを見つけて、それを外側のコンテナに追加することです.focusSection()、コンテナのサイズが内部コンテンツよりも大きくなるようにします。

5.使用例onKeyPressキーボード ショートカットを実装する

macOS および iPadOS では、複雑なインターフェイスにキーボード ショートカットを追加します。 Enter キーで確認、Esc でキャンセル、文字キーで素早く検索できます。フォーカスコンテナに入り口が追加されます.onKeyPress(.return)または.onKeyPress(characters:)修飾子。

関連セッション

コメント

GitHub Issues · utterances