Campos de texto en SwiftUI
Al terminar: Escribir un campo de texto con @State y un Binding, y evitar el error de pasar el valor en vez del enlace.
Al terminar esta lección sabrás escribir un campo de texto en SwiftUI: un cuadro donde la persona teclea, conectado a un dato con un enlace. Verás cómo se escribe, para qué sirve el $ delante del dato y qué error da Xcode si lo olvidas. No cambias Marcador: practicas en un ejercicio de tu carpeta Ejercicios.
Importa porque casi toda app pide texto en algún momento (un nombre, una búsqueda, un dorsal), y el campo de texto es el primer sitio donde el @Binding que acabas de aprender se usa de verdad: el campo escribe en tu dato cada vez que pulsas una tecla. Más adelante, el formulario de «añadir jugador» de Marcador se construye con estas mismas piezas. Temas: TextField, .textFieldStyle y dónde vive lo escrito.
El concepto · Un campo con una llave al dato
Un campo de texto (text field) es el cuadro donde el usuario escribe con el teclado. En SwiftUI se llama TextField y se crea con dos cosas: un título (también llamado etiqueta: el texto en gris que se ve dentro del campo mientras está vacío) y un enlace al String donde se guarda lo escrito (su tipo es Binding<String>: lo de entre < y > es el tipo del dato al que apunta, como en el Binding<Int> que ya viste):
TextField("Ingrediente", text: $ingrediente)El campo no guarda el texto: usa la llave que le das. Cada vez que cambias algo, escribe el texto en ingrediente, y como ingrediente es un @State, SwiftUI vuelve a dibujar lo que lo lee. Es el mismo trato que viste con los botones de gol, con otra pieza que escribe.
Ejemplo · Añadir un ingrediente
Un ejemplo solo para leer (no lo escribas en Marcador), con datos ajenos a él: el campo de una receta.
struct AnadirIngrediente: View {
@State private var ingrediente = "" // el dato: lo escrito, empieza vacío
var body: some View {
VStack(alignment: .leading, spacing: 12) {
TextField("Ingrediente", text: $ingrediente) // título en gris + la llave al dato (`$`)
.textFieldStyle(.roundedBorder) // un borde redondeado alrededor del campo
Text("Añadirás: \(ingrediente)") // lee el mismo dato que el campo escribe
}
.padding(16)
}
}Lo que se ve. Un campo con «Ingrediente» en gris y debajo Añadirás: (vacío). Al escribir «harina» letra a letra, el campo escribe en ingrediente tras cada tecla, la vista se redibuja y la línea de debajo va creciendo:
Añadirás: harinaQué hace cada pieza:
| Pieza | Para qué sirve |
|---|---|
"Ingrediente" | El título: el texto en gris mientras el campo está vacío; desaparece en cuanto escribes |
text: $ingrediente | El enlace: la llave de lectura y escritura al @State. Lo que el campo enseña es el dato, y lo que escribes va al dato |
.textFieldStyle(.roundedBorder) | Un modificador (los de SwiftUI: primeros pasos) que pone un borde redondeado. El punto delante de roundedBorder es la forma corta de escribir el valor sin repetir su tipo, como en .font(.title). Sin él, el campo se ve como el texto gris y nada más |
Compose frente a SwiftUI
| Compose | SwiftUI |
|---|---|
OutlinedTextField(value = titulo, onValueChange = { titulo = it }) | TextField("Título", text: $titulo) |
El texto baja (value) y cada tecla sube (onValueChange) | Un solo enlace de lectura y escritura |
label = { Text("Título") } | El primer parámetro, que SwiftUI enseña en gris dentro del campo mientras no hay texto y que desaparece al escribir. No sube al borde como el label de Compose: se parece más a su placeholder |
OutlinedTextField frente a TextField | .textFieldStyle(.roundedBorder) |
rememberSaveable { mutableStateOf("") } para que aguante el giro | @State private var titulo = "" |
La función onValueChange puede rechazar un texto | El campo escribe primero; si hay que rechazar algo, se corrige después (lo practicarás en el ejercicio) |
singleLine = true para una sola línea | No hace falta: el TextField de SwiftUI ya es de una sola línea |
Sobre el giro: en iOS girar el iPhone no destruye la pantalla (lo viste en El ciclo de vida de UIViewController), así que lo escrito en un @State sigue ahí al girar sin que hagas nada especial. Es la diferencia con Android, donde hacía falta rememberSaveable.
Qué pasa si olvidas el $
Es el error de olvidar el $ que viste en @Binding, ahora con un String: el campo pide una llave y le das una fotocopia.
struct Mal: View {
@State private var ingrediente = ""
var body: some View {
TextField("Ingrediente", text: ingrediente) // `ingrediente` es un String, no un Binding<String>
}
}TextField("Ingrediente", text: $ingrediente)¿Dónde vive el texto?
Igual que en Compose: en la propia vista (el @State) mientras ninguna regla de la app necesite ese texto. El campo de la receta solo lo enseña, así que se queda en la vista. Cuando una regla lo necesite («ese nombre ya existe en la plantilla»), el texto pasará a un objeto de estado, el ViewModel, que verás en MVVM: el estado, el repositorio y el caso de uso, y la vista seguirá escribiendo en él con un enlace.
Reto · Ejercicio: el saludo
En la carpeta Ejercicios de tu proyecto (la creaste en El ciclo de vida de UIViewController), crea la subcarpeta Saludo (clic derecho sobre Ejercicios → New Folder, nombre Saludo) y dentro un fichero: clic derecho sobre Saludo → New File from Template… → Swift File → Next, escribe PantallaDeSaludo como nombre y pulsa Create. Si el asistente pregunta por el objetivo (target: la app a la que se añade el fichero), deja marcado MarcadorApp.
En ese fichero, escribe una vista PantallaDeSaludo y su vista previa. Lleva:
- Un campo de texto de una sola línea con el título «Tu nombre».
- Debajo, una línea: «Hola, Ana» (con lo que haya escrito) o, si el campo está vacío, «Escribe tu nombre».
- Debajo, un contador con el formato «3/20»: caracteres escritos y máximo (los espacios cuentan). El campo no admite más de 20 caracteres.
Lo que debes ver, en la vista previa del lienzo (Editor → Canvas; si dice que está en pausa, pulsa Resume): el campo vacío, «Escribe tu nombre» y «0/20». Después, en el lienzo, toca el campo y escribe «Ana»: «Hola, Ana» y «3/20». Sigue escribiendo hasta llegar a 20: el contador marca «20/20» y los caracteres de más no se quedan. Si el lienzo de tu Xcode no responde al teclado, o se queda en pausa tras escribir (pulsa Resume, o pasa al simulador), ejecútalo en el simulador: en MarcadorApp.swift, cambia temporalmente ContentView() por PantallaDeSaludo(), pulsa Cmd+R, prueba el campo y devuelve ContentView() antes del paso.
Pistas, todo del ejemplo de la receta: el dato, el campo y su llave son los mismos. El límite no se decide al escribir (el campo ya ha escrito en el dato), sino después de que el dato cambie: la pieza que ejecuta un bloque cada vez que un dato cambia es .onChange(of:), la que usaste en La app, sus escenas y el paquete Torneo.
Piezas de String y de Swift que necesitarás:
texto.countda cuántos caracteres tiene ytexto.isEmptydice si está vacío (trueofalse).condición ? uno : otrodaunosi la condición se cumple yotrosi no; sirve para elegir entre dos textos en una sola línea.- El bloque de
.onChange(of: nombre) { _, nuevo in … }recibe dos valores, el de antes y el de ahora; con_ignoras el de antes, que aquí no hace falta. (Es la misma forma queantigua, nueva inde la lección de las escenas.) String(texto.prefix(20))da un texto con los 20 primeros caracteres. ElString(...)hace falta porqueprefixdevuelve un trozo del texto (unSubstring), que no es unStringy no se puede guardar ennombretal cual.
Ver solución
import SwiftUI
// Un saludo con campo de texto y contador. El campo admite como mucho 20 caracteres.
struct PantallaDeSaludo: View {
private let maximo = 20
@State private var nombre = ""
var body: some View {
VStack(alignment: .leading, spacing: 8) {
TextField("Tu nombre", text: $nombre)
.textFieldStyle(.roundedBorder)
Text(nombre.isEmpty ? "Escribe tu nombre" : "Hola, \(nombre)")
Text("\(nombre.count)/\(maximo)")
}
.padding(16)
// Si se pasa del máximo, se recorta a los primeros 20 caracteres.
.onChange(of: nombre) { _, nuevo in
if nuevo.count > maximo {
nombre = String(nuevo.prefix(maximo))
}
}
}
}
#Preview {
PantallaDeSaludo()
}A diferencia de Compose, aquí el campo escribe primero en el dato y la vista corrige después: con un texto de 25 caracteres pegado de golpe, el dato cambia y .onChange lo recorta a los 20 primeros caracteres (en Compose, la función onValueChange decidía antes y podía ignorarlo entero). Las dos formas son válidas; cambia cuándo se decide.
Paso · comprobar que la app no ha cambiado
Objetivo. No hay código nuevo en Marcador: el ejercicio vive en Ejercicios y la app no lo usa. Este paso es comprobar que el ejercicio funciona como dice el enunciado y que la app sigue compilando.
Compila con Cmd+B y ejecuta con Cmd+R: Marcador se ve y se comporta como al terminar @Binding. El campo de texto entrará en la app más adelante, en el formulario de añadir un jugador.
Punto de control
- En la vista previa de
PantallaDeSaludoves el campo «Tu nombre» vacío, «Escribe tu nombre» y «0/20». - Si el lienzo acepta el teclado (o si ejecutas en el simulador), escribir «Ana» da «Hola, Ana» y «3/20», y con 20 caracteres el contador marca «20/20». Si cambiaste
ContentView()enMarcadorApp.swift, está devuelto. - Cmd+B compila sin errores con la carpeta
Ejercicios/Saludodentro del proyecto. - Marcador se ve igual que antes: la app y sus pruebas no cambian.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.En una vista hay
@State private var nombre = "". ¿Cómo se conecta unTextFielda ese dato?2.Escribes
TextField("Nombre", text: nombre), sin el$. ¿Qué ocurre?3.En Compose, el campo recibe
valueyonValueChange. ¿Qué lo sustituye en SwiftUI?4.¿Para qué sirve
.textFieldStyle(.roundedBorder)en unTextField?5.Quieres que el campo no admita más de 20 caracteres. Con
TextField, ¿cómo se hace con lo que has visto?