gh-Dimillian-Skills/swiftui-ui-patterns/references/focus.md
Thomas Ricouard 70a15d08db Add SwiftUI UI patterns skill and references
Introduces the 'swiftui-ui-patterns' skill to docs/skills.json, providing best practices and example-driven guidance for building SwiftUI views and components. Adds SKILL.md and a comprehensive set of reference files covering TabView, NavigationStack, sheets, forms, controls, grids, overlays, haptics, focus handling, media, matched transitions, split views, and more.
2026-01-04 18:26:56 +01:00

2.3 KiB
Raw Permalink Blame History

Focus handling and field chaining

Intent

Use @FocusState to control keyboard focus, chain fields, and coordinate focus across complex forms.

Core patterns

  • Use an enum to represent focusable fields.
  • Set initial focus in onAppear.
  • Use .onSubmit to move focus to the next field.
  • For dynamic lists of fields, use an enum with associated values (e.g., .option(Int)).

Example: single field focus

struct AddServerView: View {
  @State private var server = ""
  @FocusState private var isServerFieldFocused: Bool

  var body: some View {
    Form {
      TextField("Server", text: $server)
        .focused($isServerFieldFocused)
    }
    .onAppear { isServerFieldFocused = true }
  }
}

Example: chained focus with enum

struct EditTagView: View {
  enum FocusField { case title, symbol, newTag }
  @FocusState private var focusedField: FocusField?

  var body: some View {
    Form {
      TextField("Title", text: $title)
        .focused($focusedField, equals: .title)
        .onSubmit { focusedField = .symbol }

      TextField("Symbol", text: $symbol)
        .focused($focusedField, equals: .symbol)
        .onSubmit { focusedField = .newTag }
    }
    .onAppear { focusedField = .title }
  }
}

Example: dynamic focus for variable fields

struct PollView: View {
  enum FocusField: Hashable { case option(Int) }
  @FocusState private var focused: FocusField?
  @State private var options: [String] = ["", ""]
  @State private var currentIndex = 0

  var body: some View {
    ForEach(options.indices, id: \.self) { index in
      TextField("Option \(index + 1)", text: $options[index])
        .focused($focused, equals: .option(index))
        .onSubmit { addOption(at: index) }
    }
    .onAppear { focused = .option(0) }
  }

  private func addOption(at index: Int) {
    options.append("")
    currentIndex = index + 1
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
      focused = .option(currentIndex)
    }
  }
}

Design choices to keep

  • Keep focus state local to the view that owns the fields.
  • Use focus changes to drive UX (validation messages, helper UI).
  • Pair with .scrollDismissesKeyboard(...) when using ScrollView/Form.

Pitfalls

  • Dont store focus state in shared objects; it is view-local.
  • Avoid aggressive focus changes during animation; delay if needed.