@FocusState — to property wrapper w SwiftUI, wprowadzony w iOS 15, który umożliwia programowe zarządzanie fokusem wprowadzania na polach tekstowych i innych elementach. Przed jego pojawieniem się programiści musieli używać UIViewRepresentable, aby uzyskać dostęp do UIKit-owych metod becomeFirstResponder i resignFirstResponder. @FocusState rozwiązuje ten problem w natywny sposób: łączysz właściwość z polem za pomocą modyfikatora .focused(), po czym ustawienie lub zresetowanie fokusu następuje przez proste przypisanie wartości. Według Apple Developer Documentation — FocusState (2025), @FocusState obsługuje dwa tryby: Bool dla prostego zarządzania (fokus jest lub go nie ma) oraz enum dla wielu pól, gdzie każdy case odpowiada konkretnemu polu wprowadzania.
Najważniejsze
@FocusState — to property wrapper, który łączy stan fokusu z konkretnym polem wprowadzania lub innym focusable-elementem w SwiftUI. W przeciwieństwie do UIKit, gdzie zarządzanie fokusem odbywa się przez metody becomeFirstResponder i resignFirstResponder, SwiftUI używa deklaratywnego podejścia: deklarujesz stan (@FocusState) i łączysz go z elementem za pomocą modyfikatora .focused(). Zmiana stanu automatycznie zmienia fokus.
Przed pojawieniem się @FocusState w iOS 15 programiści musieli tworzyć UIViewRepresentable opakowania wokół UITextField lub używać zewnętrznych bibliotek. @FocusState jest zintegrowany bezpośrednio z SwiftUI i działa z TextField, TextEditor, SecureField i SearchField. To sprawia, że kod jest czystszy, zmniejsza ilość UIKit-mostków i poprawia testowalność.
Według WWDC Session 10136 — What's new in SwiftUI (2024), @FocusState używa systemu preference-kluczy SwiftUI do przesyłania informacji o fokusie między elementami. Gdy pole otrzymuje fokus, SwiftUI automatycznie aktualizuje powiązaną właściwość @FocusState, co pozwala reagować na zmiany fokusu w kodzie.
Najprostszy sposób użycia @FocusState — typ Bool. Gdy pole jest w fokusie, właściwość ma wartość true. Gdy fokus odchodzi — false. Możesz wymusić ustawienie fokusu, przypisując true, lub zresetować go, przypisując false.
struct LoginForm: View {
@State var email = ""
@FocusState var isEmailFocused: Bool
var body: some View {
VStack {
TextField("Email", text: $email)
.focused($isEmailFocused)
Button("Pokaż klawiaturę") {
isEmailFocused = true
}
Button("Ukryj klawiaturę") {
isEmailFocused = false
}
}
}
}
W tym przykładzie isEmailFocused automatycznie staje się true, gdy użytkownik dotknie pola tekstowego, i false, gdy klawiatura jest ukrywana. Przyciski umożliwiają programowe zarządzanie fokusem — jest to przydatne dla niestandardowych klawiatur, przycisków „Dalej” i sytuacji, gdy trzeba wymusić ukrycie klawiatury po wysłaniu formularza.
Dla formularzy z wieloma polami @FocusState obsługuje enum zgodny z protokołem FocusStateValue (lub Hashable). Każdy case enum odpowiada konkretnemu polu. Pozwala to przełączać fokus między polami — na przykład przy naciśnięciu „Dalej” na klawiaturze przechodzić do następnego pola.
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() }
}
}
}
Zwóć uwagę na modyfikator .onSubmit — jest wywoływany, gdy użytkownik naciśnie „Return” na klawiaturze. Wewnątrz .onSubmit przełączamy focusedField na następne pole, co automatycznie przenosi fokus. Ostatnie pole wywołuje submitForm() w celu wysłania formularza.
@FocusState zapewnia prosty sposób ukrycia klawiatury — wystarczy ustawić właściwość na nil (dla enum) lub false (dla Bool). Czasami jednak trzeba ukryć klawiaturę bez wiązania z konkretnym polem — na przykład przy dotknięciu pustego miejsca. W tym przypadku jest kilka podejść.
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("Gotowe") {
isFocused = false
}
}
}
}
}
Modyfikator .toolbar z placement .keyboard dodaje przycisk nad klawiaturą. Jest to standardowy wzorzec UX w iOS do ukrywania klawiatury. Alternatywne podejście — użycie .onTapGesture na głównym VStack do zresetowania fokusu przy dotknięciu tła.
@FocusState doskonale łączy się z walidacją formularza. Typowy wzorzec: po naciśnięciu przycisku „Wyślij” sprawdź wszystkie pola i ustaw fokus na pierwszym polu z błędem. Poprawia to doświadczenie użytkownika — użytkownik od razu widzi, które pole wymaga poprawy.
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("Wyślij") { validateAndSubmit() }
}
}
func validateAndSubmit() {
if name.isEmpty {
focusedField = .name
return
}
if phone.isEmpty {
focusedField = .phone
return
}
// prześlij formularz
}
}
W tym przykładzie, gdy pole name jest puste, fokus przenosi się na nie, a użytkownik od razu widzi, gdzie jest błąd. Jeśli name jest wypełnione, sprawdzane jest phone. Jest to naturalne zachowanie dla formularzy — użytkownik wypełnia pola od góry do dołu, a walidacja podąża w tej samej kolejności.
Najczęstszy błąd — próba użycia @FocusState z typem niezgodnym z Hashable. @FocusState wymaga, aby typ właściwości był Hashable (Bool i opcjonalne enum już są zgodne). Jeśli próbujesz użyć niestandardowej struktury, upewnij się, że implementuje Hashable.
// ❌ Błąd: dwa @FocusState Bool zamiast enum
@FocusState var isNameFocused: Bool
@FocusState var isEmailFocused: Bool
// ✅ Poprawnie: pojedynczy enum @FocusState
enum Field: Hashable { case name; case email }
@FocusState var focusedField: Field?
Często zadawane pytania
@FocusState jest dostępny od iOS 15, iPadOS 15, macOS 12, tvOS 15 i watchOS 8. Dla projektów obsługujących iOS 14 i starsze, użyj UIViewRepresentable z UITextField i becomeFirstResponder, albo zewnętrznych bibliotek z niestandardową implementacją zarządzania fokusem.
Tak, w tym celu w niestandardowym UIViewRepresentable trzeba zaimplementować obsługę FocusState przez protokół UIViewRepresentable. Niestandardowy view musi mieć becomeFirstResponder i resignFirstResponder. SwiftUI automatycznie połączy @FocusState z tymi metodami, jeśli podasz modyfikator .focused().
W List lub Form komórki mogą być ponownie używane, co psuje połączenie @FocusState z polem. Rozwiązanie: dodaj modyfikator .id() z unikalnym identyfikatorem dla każdego TextField. Na przykład: .id(fieldName). To zmusza SwiftUI do tworzenia oddzielnej instancji View dla każdego pola.
Dodaj .onTapGesture na głównym kontenerze (VStack, ZStack) i zresetuj fokus: focusedField = nil. Jednak .onTapGesture może blokować dotknięcia na przyciski wewnątrz — użyj kontenera z .contentShape(Rectangle()) i .onTapGesture na nim, albo niestandardowego UIKitBackgroundView.
@FocusState nie zapewnia bezpośredniego API do animacji klawiatury — jest to systemowe zachowanie iOS. Możesz jednak reagować na zmiany fokusu za pomocą .onChange(of: focusedField) lub .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) do niestandardowej animacji zawartości.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również