@FocusState — это property wrapper в SwiftUI, представленный в iOS 15, который позволяет программно управлять фокусом ввода на текстовых полях и других элементах. До его появления разработчикам приходилось использовать UIViewRepresentable для доступа к UIKit-методам becomeFirstResponder и resignFirstResponder. @FocusState решает эту проблему нативным способом: вы связываете свойство с полем через модификатор .focused(), после чего установка или сброс фокуса происходит простым присваиванием значения. По данным Apple Developer Documentation — FocusState (2025), @FocusState поддерживает два режима: Bool для простого управления (фокус есть или нет) и enum для нескольких полей, где каждый case соответствует конкретному полю ввода.
Главное
@FocusState — это property wrapper, который связывает состояние фокуса с конкретным полем ввода или другим focusable-элементом в SwiftUI. В отличие от UIKit, где управление фокусом происходит через методы becomeFirstResponder и resignFirstResponder, SwiftUI использует декларативный подход: вы объявляете состояние (@FocusState) и связываете его с элементом через модификатор .focused(). Изменение состояния автоматически меняет фокус.
До появления @FocusState в iOS 15 разработчикам приходилось создавать UIViewRepresentable обёртки вокруг UITextField или использовать сторонние библиотеки. @FocusState интегрирован непосредственно в SwiftUI и работает с TextField, TextEditor, SecureField и SearchField. Это делает код чище, уменьшает количество UIKit-мостиков и улучшает тестируемость.
По данным WWDC Session 10136 — What's new in SwiftUI (2024), @FocusState использует систему preference-ключей SwiftUI для передачи информации о фокусе между элементами. Когда поле получает фокус, SwiftUI автоматически обновляет связанное @FocusState свойство, что позволяет реагировать на изменения фокуса в коде.
Простейший способ использования @FocusState — тип Bool. Когда поле в фокусе, свойство равно true. Когда фокус уходит — false. Вы можете принудительно установить фокус, присвоив true, или сбросить его, присвоив false.
struct LoginForm: View {
@State var email = ""
@FocusState var isEmailFocused: Bool
var body: some View {
VStack {
TextField("Email", text: $email)
.focused($isEmailFocused)
Button("Show Keyboard") {
isEmailFocused = true
}
Button("Hide Keyboard") {
isEmailFocused = false
}
}
}
}
В этом примере isEmailFocused автоматически становится true, когда пользователь тапает на текстовое поле, и false, когда клавиатура скрывается. Кнопки позволяют управлять фокусом программно — это полезно для кастомных клавиатур, кнопок "Далее" и ситуаций, когда нужно принудительно скрыть клавиатуру после отправки формы.
Для форм с несколькими полями @FocusState поддерживает enum, соответствующий протоколу FocusStateValue (или Hashable). Каждый case enum соответствует конкретному полю. Это позволяет переключать фокус между полями — например, при нажатии "Next" на клавиатуре перемещаться к следующему полю.
struct RegistrationForm: View {
enum Field: Hashable {
case email
case password
case confirmPassword
}
@State var email = ""
@State var password = ""
@State var confirmPassword = ""
@FocusState var focusedField: Field?
var body: some View {
Form {
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
.onSubmit { focusedField = .password }
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
.onSubmit { focusedField = .confirmPassword }
SecureField("Confirm", text: $confirmPassword)
.focused($focusedField, equals: .confirmPassword)
.onSubmit { submitForm() }
}
}
}
Обрати внимание на модификатор .onSubmit — он вызывается, когда пользователь нажимает "Return" на клавиатуре. Внутри .onSubmit мы переключаем focusedField на следующее поле, что автоматически перемещает фокус. Последнее поле вызывает submitForm() для отправки формы.
@FocusState предоставляет простой способ скрыть клавиатуру — достаточно установить свойство в nil (для enum) или false (для Bool). Однако иногда нужно скрыть клавиатуру без привязки к конкретному полю — например, при тапе на пустое место. В этом случае есть несколько подходов.
struct DismissKeyboardView: View {
@State var text = ""
@FocusState var isFocused: Bool
var body: some View {
TextField("Enter text", text: $text)
.focused($isFocused)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
isFocused = false
}
}
}
}
}
Модификатор .toolbar с placement .keyboard добавляет кнопку над клавиатурой. Это стандартный UX-паттерн в iOS для скрытия клавиатуры. Альтернативный подход — использовать .onTapGesture на корневом VStack для сброса фокуса при тапе на фон.
@FocusState отлично комбинируется с валидацией формы. Типичный паттерн: после нажатия кнопки "Отправить" проверять все поля и устанавливать фокус на первое поле с ошибкой. Это улучшает пользовательский опыт — пользователь сразу видит, какое поле нужно исправить.
struct ValidatedForm: View {
enum Field: Hashable { case name; case phone }
@State var name = ""
@State var phone = ""
@FocusState var focusedField: Field?
@State var errors: [String] = []
var body: some View {
Form {
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
TextField("Phone", text: $phone)
.focused($focusedField, equals: .phone)
Button("Submit") { validateAndSubmit() }
}
}
func validateAndSubmit() {
if name.isEmpty {
focusedField = .name
return
}
if phone.isEmpty {
focusedField = .phone
return
}
// submit form
}
}
В этом примере при пустом поле name фокус перемещается на него, и пользователь сразу видит, где ошибка. Если name заполнено, проверяется phone. Это естественное поведение для форм — пользователь заполняет поля сверху вниз, и валидация следует тому же порядку.
Наиболее частая ошибка — попытка использовать @FocusState с типом, не соответствующим Hashable. @FocusState требует, чтобы тип свойства был Hashable (Bool и опциональные enum уже соответствуют). Если вы пытаетесь использовать кастомную структуру, убедитесь, что она реализует Hashable.
// ❌ Wrong: two @FocusState Bool instead of enum
@FocusState var isNameFocused: Bool
@FocusState var isEmailFocused: Bool
// ✅ Correct: single enum @FocusState
enum Field: Hashable { case name; case email }
@FocusState var focusedField: Field?
Часто задаваемые вопросы
@FocusState доступен с iOS 15, iPadOS 15, macOS 12, tvOS 15 и watchOS 8. Для проектов, поддерживающих iOS 14 и ниже, используй UIViewRepresentable с UITextField и becomeFirstResponder, либо сторонние библиотеки с кастомной реализацией управления фокусом.
Да, для этого нужно в кастомном UIViewRepresentable реализовать поддержку FocusState через протокол UIViewRepresentable. Кастомный view должен иметь becomeFirstResponder и resignFirstResponder. SwiftUI автоматически свяжет @FocusState с этими методами, если указать .focused() модификатор.
В List или Form ячейки могут переиспользоваться, что ломает связь @FocusState с полем. Решение: добавь .id() модификатор с уникальным идентификатором для каждого TextField. Например: .id(fieldName). Это заставляет SwiftUI создавать отдельный экземпляр View для каждого поля.
Добавь .onTapGesture на корневой контейнер (VStack, ZStack) и сбрось фокус: focusedField = nil. Однако .onTapGesture может блокировать тапы на кнопки внутри — используй контейнер с .contentShape(Rectangle()) и .onTapGesture на нём, либо кастомный UIKitBackgroundView.
@FocusState не предоставляет прямого API для анимации клавиатуры — это системное поведение iOS. Однако ты можешь реагировать на изменения фокуса с помощью .onChange(of: focusedField) или .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) для кастомной анимации контента.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также