@FocusState es un property wrapper en SwiftUI, presentado en iOS 15, que permite controlar programáticamente el foco de entrada en campos de texto y otros elementos. Antes de su aparición, los desarrolladores tenían que usar UIViewRepresentable para acceder a los métodos UIKit becomeFirstResponder y resignFirstResponder. @FocusState resuelve este problema de forma nativa: vinculas una propiedad a un campo mediante el modificador .focused(), tras lo cual establecer o quitar el foco se realiza con una simple asignación de valor. Según Apple Developer Documentation — FocusState (2025), @FocusState admite dos modos: Bool para gestión simple (foco activado o desactivado) y enum para varios campos, donde cada caso corresponde a un campo de entrada específico.
Puntos clave
@FocusState es un property wrapper que vincula el estado del foco a un campo de entrada específico u otro elemento enfocable en SwiftUI. A diferencia de UIKit, donde la gestión del foco ocurre a través de los métodos becomeFirstResponder y resignFirstResponder, SwiftUI utiliza un enfoque declarativo: declaras un estado (@FocusState) y lo vinculas a un elemento mediante el modificador .focused(). Cambiar el estado cambia automáticamente el foco.
Antes de la introducción de @FocusState en iOS 15, los desarrolladores tenían que crear wrappers UIViewRepresentable alrededor de UITextField o usar bibliotecas de terceros. @FocusState está integrado directamente en SwiftUI y funciona con TextField, TextEditor, SecureField y SearchField. Esto hace que el código sea más limpio, reduce la cantidad de puentes UIKit y mejora la capacidad de prueba.
Según WWDC Session 10136 — What's new in SwiftUI (2024), @FocusState utiliza el sistema de preference keys de SwiftUI para pasar información de foco entre elementos. Cuando un campo recibe el foco, SwiftUI actualiza automáticamente la propiedad @FocusState asociada, lo que permite reaccionar a los cambios de foco en el código.
La forma más sencilla de usar @FocusState es con el tipo Bool. Cuando un campo está enfocado, la propiedad es true. Cuando se pierde el foco — false. Puedes forzar el foco asignándole true, o eliminarlo asignándole 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
}
}
}
}
En este ejemplo, isEmailFocused se vuelve automáticamente true cuando el usuario toca el campo de texto, y false cuando se oculta el teclado. Los botones permiten gestionar el foco programáticamente — útil para teclados personalizados, botones "Siguiente" y situaciones en las que necesitas ocultar forzosamente el teclado después de enviar un formulario.
Para formularios con varios campos, @FocusState admite un enum que conforme al protocolo FocusStateValue (o Hashable). Cada caso del enum corresponde a un campo específico. Esto permite cambiar el foco entre campos — por ejemplo, cuando el usuario presiona "Siguiente" en el teclado para moverse al siguiente campo.
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() }
}
}
}
Observa el modificador .onSubmit — se llama cuando el usuario presiona "Return" en el teclado. Dentro de .onSubmit cambiamos focusedField al siguiente campo, lo que mueve automáticamente el foco. El último campo llama a submitForm() para enviar el formulario.
@FocusState proporciona una forma sencilla de ocultar el teclado — solo hay que establecer la propiedad en nil (para enum) o false (para Bool). Sin embargo, a veces necesitas ocultar el teclado sin vincularlo a un campo específico — por ejemplo, al tocar un espacio vacío. En este caso, hay varios enfoques.
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
}
}
}
}
}
El modificador .toolbar con placement .keyboard añade un botón sobre el teclado. Este es un patrón UX estándar en iOS para ocultar el teclado. Un enfoque alternativo es usar .onTapGesture en el VStack raíz para restablecer el foco al tocar el fondo.
@FocusState se combina perfectamente con la validación de formularios. Un patrón típico: después de presionar el botón "Enviar", validar todos los campos y establecer el foco en el primer campo con error. Esto mejora la experiencia del usuario — el usuario ve inmediatamente qué campo debe corregir.
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
}
}
En este ejemplo, si el campo name está vacío, el foco se mueve a él, y el usuario ve inmediatamente dónde está el error. Si name está completo, se verifica phone. Este es un comportamiento natural para formularios — el usuario llena los campos de arriba a abajo, y la validación sigue el mismo orden.
El error más común es intentar usar @FocusState con un tipo que no conforme a Hashable. @FocusState requiere que el tipo de la propiedad sea Hashable (Bool y los enums opcionales ya lo cumplen). Si intentas usar una estructura personalizada, asegúrate de que implemente 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?
Preguntas frecuentes
@FocusState está disponible desde iOS 15, iPadOS 15, macOS 12, tvOS 15 y watchOS 8. Para proyectos que soporten iOS 14 y versiones anteriores, usa UIViewRepresentable con UITextField y becomeFirstResponder, o bibliotecas de terceros con implementación personalizada de gestión de foco.
Sí, para ello necesitas implementar el soporte de FocusState en el UIViewRepresentable personalizado a través del protocolo UIViewRepresentable. La vista personalizada debe tener becomeFirstResponder y resignFirstResponder. SwiftUI vinculará automáticamente @FocusState con estos métodos si especificas el modificador .focused().
En List o Form, las celdas pueden reutilizarse, lo que rompe la vinculación de @FocusState con el campo. Solución: añade el modificador .id() con un identificador único para cada TextField. Por ejemplo: .id(fieldName). Esto obliga a SwiftUI a crear una instancia de Vista separada para cada campo.
Añade .onTapGesture al contenedor raíz (VStack, ZStack) y restablece el foco: focusedField = nil. Sin embargo, .onTapGesture puede bloquear los toques en los botones internos — usa un contenedor con .contentShape(Rectangle()) y .onTapGesture en él, o un UIKitBackgroundView personalizado.
@FocusState no proporciona una API directa para la animación del teclado — esto es comportamiento del sistema iOS. Sin embargo, puedes reaccionar a los cambios de foco con .onChange(of: focusedField) o .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) para animación personalizada del contenido.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también