Saltar al contenido
K
iOS con SwiftUIConcepto25 min Intermedio

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.

ejemplo mínimo: un campo para añadir un ingrediente (con comentarios)
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: harina

Qué hace cada pieza:

PiezaPara qué sirve
"Ingrediente"El título: el texto en gris mientras el campo está vacío; desaparece en cuanto escribes
text: $ingredienteEl 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

ComposeSwiftUI
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 textoEl campo escribe primero; si hay que rechazar algo, se corrige después (lo practicarás en el ejercicio)
singleLine = true para una sola líneaNo 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.

con el bug: pasar el valor donde se espera el enlace
struct Mal: View {
    @State private var ingrediente = ""
    var body: some View {
        TextField("Ingrediente", text: ingrediente)   // `ingrediente` es un String, no un Binding<String>
    }
}
corregido: el prefijo $ pasa el enlace
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:

  1. Un campo de texto de una sola línea con el título «Tu nombre».
  2. Debajo, una línea: «Hola, Ana» (con lo que haya escrito) o, si el campo está vacío, «Escribe tu nombre».
  3. 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.count da cuántos caracteres tiene y texto.isEmpty dice si está vacío (true o false).
  • condición ? uno : otro da uno si la condición se cumple y otro si 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 que antigua, nueva in de la lección de las escenas.)
  • String(texto.prefix(20)) da un texto con los 20 primeros caracteres. El String(...) hace falta porque prefix devuelve un trozo del texto (un Substring), que no es un String y no se puede guardar en nombre tal cual.
Ver solución
Ejercicios/Saludo/PantallaDeSaludo.swift
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 PantallaDeSaludo ves 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() en MarcadorApp.swift, está devuelto.
  • Cmd+B compila sin errores con la carpeta Ejercicios/Saludo dentro 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. 1.En una vista hay @State private var nombre = "". ¿Cómo se conecta un TextField a ese dato?

  2. 2.Escribes TextField("Nombre", text: nombre), sin el $. ¿Qué ocurre?

  3. 3.En Compose, el campo recibe value y onValueChange. ¿Qué lo sustituye en SwiftUI?

  4. 4.¿Para qué sirve .textFieldStyle(.roundedBorder) en un TextField?

  5. 5.Quieres que el campo no admita más de 20 caracteres. Con TextField, ¿cómo se hace con lo que has visto?

Faltan 5 de 5