View in English

  • Apple Developer
    • Get Started

    Explore Get Started

    • Overview
    • Learn
    • Apple Developer Program

    Stay Updated

    • Latest News
    • Hello Developer
    • Platforms

    Explore Platforms

    • Apple Platforms
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store

    Featured

    • Design
    • Distribution
    • Games
    • Accessories
    • Web
    • Home
    • CarPlay
    • Technologies

    Explore Technologies

    • Overview
    • Xcode
    • Swift
    • SwiftUI

    Featured

    • Accessibility
    • App Intents
    • Apple Intelligence
    • Games
    • Machine Learning & AI
    • Security
    • Xcode Cloud
    • Community

    Explore Community

    • Overview
    • Meet with Apple events
    • Community-driven events
    • Developer Forums
    • Open Source

    Featured

    • WWDC
    • Swift Student Challenge
    • Developer Stories
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Centers
    • Documentation

    Explore Documentation

    • Documentation Library
    • Technology Overviews
    • Sample Code
    • Human Interface Guidelines
    • Videos

    Release Notes

    • Featured Updates
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • tvOS
    • Xcode
    • Downloads

    Explore Downloads

    • All Downloads
    • Operating Systems
    • Applications
    • Design Resources

    Featured

    • Xcode
    • TestFlight
    • Fonts
    • SF Symbols
    • Icon Composer
    • Support

    Explore Support

    • Overview
    • Help Guides
    • Developer Forums
    • Feedback Assistant
    • Contact Us

    Featured

    • Account Help
    • App Review Guidelines
    • App Store Connect Help
    • Upcoming Requirements
    • Agreements and Guidelines
    • System Status
  • Quick Links

    • Events
    • News
    • Forums
    • Sample Code
    • Videos
 

Vidéos

Ouvrir le menu Fermer le menu
  • Collections
  • Toutes les vidéos
  • À propos

Plus de vidéos

  • À propos
  • Code
  • The SwiftUI cookbook for focus

    The SwiftUI team is back in the coding "kitchen" with powerful tools to shape your app's focus experience. Join us and learn about the staple ingredients that support focus-driven interactions in your app. Discover focus interactions for custom views, find out about key-press handlers for keyboard input, and learn how to support movement and hierarchy with focus sections. We'll also go through some tasty recipes for common focus patterns in your app.

    Chapitres

    • 1:44 - What is focus
    • 3:18 - Ingredients
    • 3:35 - Ingredients: Focusable views
    • 6:04 - Ingredients: Focus state
    • 7:03 - Ingredients: Focused values
    • 8:54 - Ingredients: Focused sections
    • 10:45 - Recipes
    • 11:21 - Recipes: Controlling focus
    • 14:36 - Recipes: Custom focusable control
    • 18:04 - Recipes: Grid view

    Ressources

      • Vidéo HD
      • Vidéo SD

    Vidéos connexes

    WWDC23

    • What’s new in SwiftUI
  • Rechercher dans cette vidéo…
    • 5:05 - Focusable views

      // Focusable views
      
      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)
          }
      }
    • 6:12 - Focus state

      // Focus state
      
      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)
          }
      }
    • 7:32 - Focused values

      // 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) { /* ... */ }
      }
      
      struct Recipe: Hashable, Identifiable {
          let id = UUID()
          var title = ""
          var isFavorite = false
      }
    • 10:03 - Focus sections

      // Focus sections
      
      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) { /* ... */ }
      }
      
      struct Recipe: Hashable, Identifiable {
          static let examples: [Recipe] = [
              Recipe(name: "Apple Pie"),
              Recipe(name: "Baklava"),
              Recipe(name: "Crème Brûlée")
          ]
      
          let id = UUID()
          var name = ""
          var imageName = ""
      }
    • 11:29 - Controlling focus

      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
          }
      }
      
      struct GroceryList: Codable {
          static let examples = GroceryList(items: [
              GroceryList.Item(name: "Apples"),
              GroceryList.Item(name: "Lasagna"),
              GroceryList.Item(name: "")
          ])
      
          struct Item: Codable, Hashable, Identifiable {
              var id = UUID()
              var name: String
              var isObtained: Bool = false
          }
      
          var items: [Item] = []
      
          mutating func addItem() -> Item {
              let item = GroceryList.Item(name: "")
              items.append(item)
              return item
          }
      }
      
      struct ChecklistToggleStyle: ToggleStyle {
          func makeBody(configuration: Configuration) -> some View {
              Button {
                  configuration.isOn.toggle()
              } label: {
                  Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle.dashed")
                      .foregroundStyle(configuration.isOn ? .green : .gray)
                      .font(.system(size: 20))
                      .contentTransition(.symbolEffect)
                      .animation(.linear, value: configuration.isOn)
              }
              .buttonStyle(.plain)
              .contentShape(.circle)
          }
      }
      
      extension ToggleStyle where Self == ChecklistToggleStyle {
          static var checklist: ChecklistToggleStyle { .init() }
      }
    • 15:25 - Custom focusable control

      struct RatingPicker: View {
          @Environment(\.layoutDirection) private var layoutDirection
          @Binding var rating: Rating?
      
          #if os(watchOS)
          @State private var digitalCrownRotation = 0.0
          #endif
      
          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
                  }
              }
          }
      
          #if os(macOS)
          private func selectRating(
              _ direction: MoveCommandDirection, layoutDirection: LayoutDirection
          ) {
              var direction = direction
      
              if layoutDirection == .rightToLeft {
                  switch direction {
                  case .left: direction = .right
                  case .right: direction = .left
                  default: break
                  }
              }
      
              if let rating {
                  switch direction {
                  case .left:
                      guard let previousRating = rating.previous else { return }
                      self.rating = previousRating
                  case .right:
                      guard let nextRating = rating.next else { return }
                      self.rating = nextRating
                  default:
                      break
                  }
              }
          }
          #endif
      }
      
      private struct EmojiContainer<Content: View>: View {
          @Environment(\.isFocused) private var isFocused
          private var content: Content
      
          #if os(watchOS)
          private var strokeColor: Color {
              isFocused ? .green : .clear
          }
          #endif
      
          init(@ViewBuilder content: @escaping () -> Content) {
              self.content = content()
          }
      
          var body: some View {
              HStack(spacing: 2) {
                  content
              }
                  .frame(height: 32)
                  .font(.system(size: 24))
                  .padding(.horizontal, 8)
                  .padding(.vertical, 6)
                  .background(.quaternary)
                  .clipShape(.capsule)
                  #if os(watchOS)
                  .overlay(
                      Capsule()
                          .strokeBorder(strokeColor, lineWidth: 1.5)
                  )
                  #endif
          }
      }
      
      private struct EmojiView: View {
          var rating: Rating
          var isSelected: Bool
          var action: () -> Void
      
          var body: some View {
              ZStack {
                  Circle()
                      .fill(isSelected ? Color.accentColor : Color.clear)
                  Text(verbatim: rating.emoji)
                      .onTapGesture { action() }
                      .accessibilityLabel(rating.localizedName)
              }
          }
      }
      
      enum Rating: Int, CaseIterable, Identifiable {
          case meh
          case yummy
          case delicious
      
          var id: RawValue { rawValue }
      
          var emoji: String {
              switch self {
              case .meh:
                  return "😕"
              case .yummy:
                  return "🙂"
              case .delicious:
                  return "🥰"
              }
          }
      
          var localizedName: LocalizedStringKey {
              switch self {
              case .meh:
                  return "Meh"
              case .yummy:
                  return "Yummy"
              case .delicious:
                  return "Delicious"
              }
          }
      
          var previous: Rating? {
              let ratings = Rating.allCases
              let index = ratings.firstIndex(of: self)!
      
              guard index != ratings.startIndex else {
                  return nil
              }
      
              let previousIndex = ratings.index(before: index)
              return ratings[previousIndex]
          }
      
          var next: Rating? {
              let ratings = Rating.allCases
              let index = ratings.firstIndex(of: self)!
      
              let nextIndex = ratings.index(after: index)
              guard nextIndex != ratings.endIndex else {
                  return nil
              }
      
              return ratings[nextIndex]
          }
      }
    • 18:50 - Grid view

      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)
              }
          }
      
          private var columns: [GridItem] {
              [ GridItem(.adaptive(minimum: RecipeTile.size), spacing: 0) ]
          }
      
          private func navigateToRecipe(id: Recipe.ID) {
              // ...
          }
      
          private func selectRecipe(
              _ direction: MoveCommandDirection, layoutDirection: LayoutDirection
          ) {
              // ...
          }
      
          private func selectRecipe(matching characters: String) -> KeyPress.Result {
              // ...
              return .handled
          }
      }
      
      struct RecipeTile: View {
          static let size = 240.0
          static let selectionStrokeWidth = 4.0
      
          var recipe: Recipe
          var isSelected: Bool
      
          private var strokeStyle: AnyShapeStyle {
              isSelected
                  ? AnyShapeStyle(.selection)
                  : AnyShapeStyle(.clear)
          }
      
          var body: some View {
              VStack {
                  RoundedRectangle(cornerRadius: 20)
                      .fill(.background)
                      .strokeBorder(
                          strokeStyle,
                          lineWidth: Self.selectionStrokeWidth)
                      .frame(width: Self.size, height: Self.size)
                  Text(recipe.name)
              }
          }
      }
      
      struct SelectedRecipeKey: FocusedValueKey {
          typealias Value = Binding<Recipe.ID>
      }
      
      extension FocusedValues {
          var selectedRecipe: Binding<Recipe.ID>? {
              get { self[SelectedRecipeKey.self] }
              set { self[SelectedRecipeKey.self] = newValue }
          }
      }
      
      struct RecipeCommands: Commands {
          @FocusedBinding(\.selectedRecipe) private var selectedRecipe: Recipe.ID?
      
          var body: some Commands {
              CommandMenu("Recipe") {
                  Button("Add to Grocery List") {
                      if let selectedRecipe {
                          addRecipe(selectedRecipe)
                      }
                  }
                  .disabled(selectedRecipe == nil)
              }
          }
      
          private func addRecipe(_ recipe: Recipe.ID) { /* ... */ }
      }
      
      struct Recipe: Hashable, Identifiable {
          static let examples: [Recipe] = [
              Recipe(name: "Apple Pie"),
              Recipe(name: "Baklava"),
              Recipe(name: "Crème Brûlée")
          ]
      
          let id = UUID()
          var name = ""
          var imageName = ""
      }
    • 21:28 - Focusable grid on tvOS

      struct ContentView: View {
          var body: some View {
              HStack {
                  VStack {
                      List(["Dessert", "Pancake", "Salad", "Sandwich"], id: \.self) {
                          NavigationLink($0, destination: Color.gray)
                      }
                      Spacer()
                  }
                  .focusSection()
      
                  ScrollView {
                      LazyVGrid(columns: [GridItem(), GridItem()]) {
                          RoundedRectangle(cornerRadius: 5.0)
                              .focusable()
                      }
                  }
                  .focusSection()
              }
          }
      }

Developer Footer

  • Vidéos
  • WWDC23
  • The SwiftUI cookbook for focus
  • Open Menu Close Menu
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • Icon Composer
    • SF Symbols
    Open Menu Close Menu
    • Accessibility
    • Accessories
    • Apple Intelligence
    • Audio & Video
    • Augmented Reality
    • Business
    • Design
    • Distribution
    • Education
    • Games
    • Health & Fitness
    • In-App Purchase
    • Localization
    • Maps & Location
    • Machine Learning & AI
    • Security
    • Safari & Web
    Open Menu Close Menu
    • Documentation
    • Downloads
    • Sample Code
    • Videos
    Open Menu Close Menu
    • Help Guides & Articles
    • Contact Us
    • Forums
    • Feedback & Bug Reporting
    • System Status
    Open Menu Close Menu
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles
    • Feedback Assistant
    Open Menu Close Menu
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program
    • Mini Apps Partner Program
    • News Partner Program
    • Video Partner Program
    • Security Bounty Program
    • Security Research Device Program
    Open Menu Close Menu
    • Meet with Apple
    • Apple Developer Centers
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Academies
    • WWDC
    Read the latest news.
    Get the Apple Developer app.
    Copyright © 2026 Apple Inc. All rights reserved.
    Terms of Use Privacy Policy Agreements and Guidelines