From apple-dev
SwiftUI focus and keyboard management including @FocusState, FocusedValue, keyboard toolbar, submit triggers, and text field navigation. Use when managing focus, keyboard behavior, or text input flow.
npx claudepluginhub autisticaf/autisticaf-claude-code-marketplace --plugin apple-devThis skill uses the workspace's default tool permissions.
> **First step:** Tell the user: "swiftui-focus-management skill loaded."
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
First step: Tell the user: "swiftui-focus-management skill loaded."
Patterns for controlling focus, keyboard behavior, and text input navigation in SwiftUI. Covers single-field focus, multi-field form navigation, keyboard toolbars, submit handling, and cross-platform focus APIs.
Use this skill when the user:
What focus behavior do you need?
|
+- Single text field focus toggle
| +- @FocusState with Bool -> Single Field section
|
+- Navigate between multiple fields
| +- @FocusState with enum -> Multi-Field section
|
+- Keyboard toolbar (Done, Next, Previous)
| +- .toolbar { ToolbarItemGroup(placement: .keyboard) }
|
+- Customize return key action
| +- .onSubmit + .submitLabel -> Submit section
|
+- macOS menu bar commands reading focused data
| +- FocusedValue / FocusedBinding -> Menu Commands section
|
+- Make non-text view focusable
| +- .focusable() -> Non-Text Focus section
|
+- Set initial focus on appear
| +- .defaultFocus() -> Default Focus section
| API | Minimum Version | Notes |
|---|---|---|
@FocusState | iOS 15 | Bool or Hashable enum binding |
.focused(_:) | iOS 15 | Bool-based focus binding |
.focused(_:equals:) | iOS 15 | Enum-based focus binding |
.onSubmit | iOS 15 | Return key action |
.submitLabel(_:) | iOS 15 | .done, .next, .search, etc. |
FocusedValue | iOS 14 | Read-only value from focused view |
FocusedBinding | iOS 15 | Writable binding from focused view |
.focusable() | iOS 14 | Make non-text views focusable |
.focusSection() | tvOS 15 | Group views for spatial focus |
.defaultFocus(_:_:) | iOS 16 / macOS 13 | Set initial focus target |
.prefersDefaultFocus(_:in:) | tvOS 15 / macOS 13 | Preferred focus in scope |
@FocusedObject | iOS 16 | ObservableObject from focused view |
.focusScope(_:) | tvOS 15 / macOS 13 | Define a focus scope namespace |
Use a Bool binding when you have one field to toggle focus on and off.
struct SearchBar: View {
@State private var query = ""
@FocusState private var isSearchFocused: Bool
var body: some View {
VStack {
TextField("Search...", text: $query)
.focused($isSearchFocused)
.onSubmit { performSearch() }
Button("Cancel") {
isSearchFocused = false // dismisses keyboard
}
}
.onAppear {
isSearchFocused = true // auto-focus on appear
}
}
}
Use a Hashable enum when navigating between multiple fields.
struct SignUpForm: View {
enum Field: Hashable {
case username, email, password
}
@State private var username = ""
@State private var email = ""
@State private var password = ""
@FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .username)
.submitLabel(.next)
.onSubmit { focusedField = .email }
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
.submitLabel(.next)
.onSubmit { focusedField = .password }
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
.submitLabel(.done)
.onSubmit { submit() }
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Previous") { moveFocus(-.1) }
Button("Next") { moveFocus(+.1) }
Spacer()
Button("Done") { focusedField = nil }
}
}
}
private func moveFocus(_ direction: Double) {
let allFields: [Field] = [.username, .email, .password]
guard let current = focusedField,
let index = allFields.firstIndex(of: current) else { return }
let next = direction > 0 ? index + 1 : index - 1
focusedField = allFields.indices.contains(next) ? allFields[next] : nil
}
}
Control the return key label and action.
TextField("Search", text: $query)
.submitLabel(.search) // shows "Search" on return key
.onSubmit { performSearch() }
// Available submit labels:
// .done, .go, .join, .next, .return, .route, .search, .send, .continue
Nested .onSubmit propagates up the view hierarchy:
Form {
TextField("Name", text: $name)
TextField("Bio", text: $bio)
}
.onSubmit { saveProfile() } // called for any field in the form
Add buttons above the keyboard using ToolbarItemGroup(placement: .keyboard).
struct NotesEditor: View {
@State private var text = ""
@FocusState private var isFocused: Bool
var body: some View {
TextEditor(text: $text)
.focused($isFocused)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button(action: insertBullet) {
Image(systemName: "list.bullet")
}
Spacer()
Button("Done") { isFocused = false }
}
}
}
}
Expose data from the focused view to menu bar commands.
// 1. Define the focused value key
struct FocusedDocumentKey: FocusedValueKey {
typealias Value = Binding<String>
}
extension FocusedValues {
var documentText: Binding<String>? {
get { self[FocusedDocumentKey.self] }
set { self[FocusedDocumentKey.self] = newValue }
}
}
// 2. Publish from the focused view
struct EditorView: View {
@State private var text = ""
var body: some View {
TextEditor(text: $text)
.focusedValue(\.documentText, $text)
}
}
// 3. Read in menu commands
struct AppCommands: Commands {
@FocusedBinding(\.documentText) var text
var body: some Commands {
CommandMenu("Format") {
Button("Uppercase") {
text = text?.uppercased() ?? ""
}
.disabled(text == nil)
}
}
}
Make buttons, images, or custom views participate in focus navigation.
struct FocusableCard: View {
@FocusState private var isFocused: Bool
var body: some View {
RoundedRectangle(cornerRadius: 12)
.fill(isFocused ? Color.blue : Color.gray)
.frame(width: 200, height: 120)
.focusable()
.focused($isFocused)
.onKeyPress(.return) {
handleSelection()
return .handled
}
}
}
Group views into a single focusable region so the focus engine treats them as a unit.
HStack {
VStack {
Button("A") {}
Button("B") {}
}
.focusSection() // focus moves into/out of this group as a unit
VStack {
Button("C") {}
Button("D") {}
}
.focusSection()
}
Set which view receives focus when a scene or scope first appears.
struct SettingsPanel: View {
@FocusState private var focusedField: Field?
@Namespace private var settingsScope
var body: some View {
Form {
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
}
.focusScope(settingsScope)
.defaultFocus($focusedField, .name, priority: .userInitiated)
}
}
For tvOS or complex layouts, use .prefersDefaultFocus within a scope:
VStack {
Button("Primary") { }
.prefersDefaultFocus(in: scopeNamespace)
Button("Secondary") { }
}
.focusScope(scopeNamespace)
Set @FocusState to nil (enum) or false (Bool) to resign first responder.
// Tap-outside-to-dismiss pattern
struct DismissableForm: View {
@FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
}
.onTapGesture { focusedField = nil }
// Or use scrollDismissesKeyboard for scroll views:
.scrollDismissesKeyboard(.interactively) // iOS 16+
}
}
// ✅ Use Optional enum for multi-field — nil means no focus
@FocusState private var field: Field?
// ✅ Dismiss keyboard by setting focus to nil
Button("Done") { focusedField = nil }
// ✅ Chain submitLabel with onSubmit for clear field progression
TextField("Email", text: $email)
.submitLabel(.next)
.onSubmit { focusedField = .password }
// ✅ Use .defaultFocus to set initial field on appear
.defaultFocus($focusedField, .username, priority: .userInitiated)
// ✅ Provide keyboard toolbar for fields without a return key (number pad)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") { focusedField = nil }
}
}
// ❌ Don't use DispatchQueue to set focus — use @FocusState directly
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
UIApplication.shared.sendAction(#selector(UIResponder.becomeFirstResponder),
to: nil, from: nil, for: nil)
}
// ❌ Don't use UIKit responder chain to dismiss keyboard in SwiftUI
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
// ❌ Don't forget that @FocusState enum must be Optional
@FocusState private var field: Field // Compiler error — must be Field?
// ❌ Don't use .focused() without a matching @FocusState property
TextField("Name", text: $name)
.focused($someStateVar, equals: .name) // $someStateVar must be @FocusState
// ❌ Don't set @FocusState in init or outside the view lifecycle
init() {
_focusedField = .init(wrappedValue: .username) // won't work as expected
}