@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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також