Formularios en SwiftUI: el estado y las reglas
Al terminar: Guardar el estado del formulario en un struct con campos tocados y calcular los errores como un tipo, con pruebas.
Al terminar, Marcador tendrá el cerebro del formulario «Añadir jugador», todavía sin pantalla: un struct con lo que el usuario escribió y qué campos ha tocado, las reglas que calculan los errores, y un ViewModel que las aplica con la plantilla guardada. Lo comprobarás con siete pruebas nuevas de las reglas: el recuento de pruebas unitarias sube de 35 a 42. La pantalla llega en la lección siguiente.
Importa porque un formulario que valida mal es peor que uno sin validar: o grita en rojo antes de que el usuario escriba, o deja guardar un dorsal repetido. Y porque la forma que le des al estado decide si el resto sale sencillo. Temas: dónde vive el texto de un formulario, el campo «tocado» con didSet, errores que se calculan en vez de guardarse, reglas que consultan lo ya guardado, formulario frente a dominio, y una prueba parametrizada. Nivel 2: usar.
Dónde vive el texto de un formulario
Hay dos sitios posibles. En la pantalla (un @State), si nadie más necesita lo escrito: así hiciste el saludo de Campos de texto en SwiftUI. Y en el ViewModel, si una regla tiene que consultarlo y consultar otras cosas. Aquí hay una regla así: «un dorsal no puede repetirse en el equipo», y para saberlo hace falta la plantilla guardada, que vive en el repositorio. Por eso el formulario irá en el ViewModel.
Dentro del ViewModel el texto va en un struct, FormularioJugador, con tres cosas:
- Lo escrito, tal cual, siempre como texto: mientras alguien teclea un dorsal, «1», «12», «12a» y «» son todos valores posibles. Convertirlo a número se hace al final.
- Qué campos están tocados: «tocado» quiere decir «el usuario ya escribió algo ahí». Antes de eso no se enseña ningún error: nadie quiere ver «Escribe un nombre» en rojo nada más abrir la pantalla.
- Ningún error. Los errores no se guardan: se calculan cada vez a partir del texto y de la plantilla.
Un error guardado puede quedarse viejo: escribes un nombre repetido, se guarda «repetido», cambias el nombre y se te olvida borrar el error. Un error calculado no puede quedarse viejo, porque se vuelve a calcular con lo que haya escrito en ese momento. El tipo del error es un enum: «ninguno» (nil) o uno de estos motivos, y la pantalla decide el texto de cada uno. Las reglas no saben nada de textos.
Ejemplo: el alta de un socio de un club
Un ejemplo con datos ajenos a Marcador, para leer y ejecutar en el Playground: dar de alta a un socio con un correo y una edad. Mira cómo cada pieza hace una sola cosa:
import Foundation // trae `trimmingCharacters`
// Los errores posibles de cada campo, como un tipo: ninguno, o uno de estos casos.
enum ErrorCorreo { case vacio, sinArroba }
enum ErrorEdad { case noEsNumero, menorDeEdad }
struct FormularioSocio {
// `didSet` corre DESPUÉS de cada cambio; `oldValue` es el valor de antes. Solo cuenta como
// tocado si el texto cambió de verdad: al tocar el campo, SwiftUI puede volver a escribir el
// MISMO texto, y eso no es escribir.
var correo = "" { didSet { if correo != oldValue { correoTocado = true } } }
var edad = "" { didSet { if edad != oldValue { edadTocado = true } } }
private(set) var correoTocado = false // «el usuario ya escribió aquí»
private(set) var edadTocado = false
// Los errores NO se guardan: se calculan cada vez a partir de lo escrito.
func errorCorreo() -> ErrorCorreo? {
let limpio = correo.trimmingCharacters(in: .whitespaces)
if limpio.isEmpty { return .vacio }
if !limpio.contains("@") { return .sinArroba }
return nil
}
func errorEdad() -> ErrorEdad? {
guard let numero = Int(edad.trimmingCharacters(in: .whitespaces)) else { return .noEsNumero }
return numero < 18 ? .menorDeEdad : nil
}
// Lo que se enseña: el error solo si el campo está tocado.
func correoVisible() -> ErrorCorreo? { correoTocado ? errorCorreo() : nil }
func edadVisible() -> ErrorEdad? { edadTocado ? errorEdad() : nil }
func esValido() -> Bool { errorCorreo() == nil && errorEdad() == nil }
}
// Una línea de texto con lo que vería el usuario. Sobre un OPCIONAL, `.map { … }` transforma el
// valor si lo hay (aquí, el error pasa a texto) y deja `nil` si no; `?? "sin error"` pone otro
// texto si no hay ninguno. No es el `map` de las listas: no recorre nada.
func resumen(_ f: FormularioSocio) -> String {
let correo = f.correoVisible().map { "\($0)" } ?? "sin error"
let edad = f.edadVisible().map { "\($0)" } ?? "sin error"
return "correo: \(correo) · edad: \(edad) · válido: \(f.esValido())"
}
let recienAbierto = FormularioSocio()
print("1.", resumen(recienAbierto))
var soloCorreo = FormularioSocio()
soloCorreo.correo = "ana.club.com"
print("2.", resumen(soloCorreo))
var menor = FormularioSocio()
menor.correo = "ana@club.com"
menor.edad = "16"
print("3.", resumen(menor))
var bueno = FormularioSocio()
bueno.correo = "ana@club.com"
bueno.edad = "30"
print("4.", resumen(bueno))1. correo: sin error · edad: sin error · válido: false
2. correo: sinArroba · edad: sin error · válido: false
3. correo: sin error · edad: menorDeEdad · válido: false
4. correo: sin error · edad: sin error · válido: trueUna nota sobre la salida: al meter un caso de enum en un texto ("\($0)"), Swift escribe su nombre, por eso sale sinArroba o menorDeEdad. Y la primera línea es la que más enseña: el formulario recién abierto no es válido (el correo está vacío), pero no enseña ningún error, porque ningún campo está tocado. En la segunda solo se ha escrito el correo, y solo ese campo da error; la edad, sin tocar, calla aunque esté mal. En la tercera, el correo es válido y la edad no.
Qué hace cada plataforma
Android y iOS resuelven lo mismo con piezas distintas. Las reglas y los textos son idénticos:
| Idea | Android | iOS |
|---|---|---|
| Lo escrito y los campos tocados | una data class que el ViewModel copia con copy(...) al escribir | un struct que se modifica en su sitio; didSet marca el campo tocado |
| Dónde está el estado | mutableStateOf dentro del ViewModel | una propiedad de un ViewModel @Observable |
| Los errores | enum class ErrorNombre { VACIO, LARGO, REPETIDO } y errorNombre(plantilla) | enum ErrorNombre { case vacio, largo, repetido } y errorNombre(en:) |
| La plantilla guardada | llega de Room como un Flow y se copia a un mutableStateOf | se lee del repositorio, que ya es @Observable, en cada consulta |
La última fila es la diferencia que ahorra código: en iOS no hay un Flow que recoger ni nada que copiar.
Reto · Ejercicio: las reglas de una reserva de mesa
Piezas que necesitas (todas son de Swift y caben en una línea):
limpio.count: cuántos caracteres tiene un texto («612345678».count es 9).(1...12).contains(numero): un rango del 1 al 12, con los dos extremos incluidos, y la pregunta de si el número está dentro.limpio.allSatisfy { $0.isNumber }: «¿todos los caracteres cumplen esto?»; aquí, «¿todos son cifras?». (También valdríaInt(limpio) != nil, aunque aceptaría un signo como «-».)- En una lista de parejas como
[("4", "612"), ("", "")], elfor caso in casoste da la pareja entera y se leen sus partes concaso.0(la primera) ycaso.1(la segunda).
En tu Playground (reutiliza el mismo y borra lo anterior), escribe las reglas de una reserva de mesa, con la misma forma que el ejemplo. Un struct ReservaDeMesa con dos textos, personas y telefono, y para cada uno si está tocado (marcado al escribir, con didSet, solo si el texto cambia: if personas != oldValue). Las reglas:
- Personas: es obligatorio, y tiene que ser un número del 1 al 12. Su error es
ErrorPersonas, con tres motivos:vacio,noEsNumeroyfueraDeRango. - Teléfono: es opcional; vacío es válido. Si hay algo, tiene que tener exactamente 9 cifras. Su error es
ErrorTelefono, con un solo motivo:noSonNueveCifras.
Dos funciones, errorPersonas() y errorTelefono(), que devuelven el error o nil. Es la misma reserva que escribiste en Kotlin en Formularios: el estado y las reglas, con los mismos datos. Prueba estas cinco reservas, en este orden, imprimiendo el error de cada una (o nil), y después comprueba que una reserva recién creada no está tocada y que escribir «2» en personas marca solo ese campo (el teléfono sigue sin tocar). Lo que debe salir:
personas «4», teléfono «612345678» -> nil / nil
personas «», teléfono «» -> vacio / nil
personas «seis», teléfono «612345678» -> noEsNumero / nil
personas «13», teléfono «» -> fueraDeRango / nil
personas «2», teléfono «6123» -> nil / noSonNueveCifras
tocada al abrir: false
tocada tras escribir: true · teléfono: falseLas cinco reservas son ("4", "612345678"), ("", ""), ("seis", "612345678"), ("13", "") y ("2", "6123").
Ver solución
import Foundation
enum ErrorPersonas { case vacio, noEsNumero, fueraDeRango }
enum ErrorTelefono { case noSonNueveCifras }
struct ReservaDeMesa {
var personas = "" { didSet { if personas != oldValue { personasTocado = true } } }
var telefono = "" { didSet { if telefono != oldValue { telefonoTocado = true } } }
private(set) var personasTocado = false
private(set) var telefonoTocado = false
func errorPersonas() -> ErrorPersonas? {
let limpio = personas.trimmingCharacters(in: .whitespaces)
if limpio.isEmpty { return .vacio }
guard let numero = Int(limpio) else { return .noEsNumero }
return (1...12).contains(numero) ? nil : .fueraDeRango
}
// El teléfono es opcional: vacío es válido. Si hay algo, tiene que ser de 9 cifras.
func errorTelefono() -> ErrorTelefono? {
let limpio = telefono.trimmingCharacters(in: .whitespaces)
if limpio.isEmpty { return nil }
let sonCifras = limpio.allSatisfy { $0.isNumber }
return (limpio.count == 9 && sonCifras) ? nil : .noSonNueveCifras
}
}
let casos = [("4", "612345678"), ("", ""), ("seis", "612345678"), ("13", ""), ("2", "6123")]
for caso in casos {
var reserva = ReservaDeMesa()
reserva.personas = caso.0
reserva.telefono = caso.1
// `.map { "\($0)" }` pasa el error a texto (el nombre del caso) si lo hay; `?? "nil"` es lo que se imprime si no hay.
let errorDePersonas = reserva.errorPersonas().map { "\($0)" } ?? "nil"
let errorDeTelefono = reserva.errorTelefono().map { "\($0)" } ?? "nil"
print("personas «\(reserva.personas)», teléfono «\(reserva.telefono)» -> \(errorDePersonas) / \(errorDeTelefono)")
}
var nueva = ReservaDeMesa()
print("tocada al abrir:", nueva.personasTocado)
nueva.personas = "2"
print("tocada tras escribir:", nueva.personasTocado, "· teléfono:", nueva.telefonoTocado)El error de las personas se decide con un orden: primero vacío, después «no es número» y al final el rango. El primer motivo que se cumple gana, y por eso "" es vacio y no noEsNumero. En el teléfono, vacío es válido: la regla de «opcional» es la primera línea.
Paso 1 · Las reglas: FormularioJugador.swift
Objetivo: el struct con el texto de los dos campos, los campos tocados y las reglas. Todavía no lo usa nadie.
Crea FormularioJugador.swift: clic derecho sobre Plantilla/UI (la carpeta que creaste en la lección anterior, porque el formulario es de la plantilla) → New File from Template… → Swift File, solo con el target MarcadorApp. Va en UI y no en Domain porque son reglas de la pantalla (qué campo tocó el usuario, qué error enseñar), no del fútbol.
Las reglas de Marcador, que son las mismas de Android:
| Campo | Regla | Texto del error (lo pone la pantalla) |
|---|---|---|
| Nombre | obligatorio, tras quitar los espacios de los bordes | «Escribe un nombre» |
| Nombre | como mucho 25 letras | «Como mucho 25 letras» |
| Nombre | que no esté ya en la plantilla, sin distinguir mayúsculas | «Ya hay un jugador con ese nombre» |
| Dorsal | opcional: vacío es válido | — |
| Dorsal | si hay algo, un número | «Escribe solo números» |
| Dorsal | del 1 al 99 | «Del 1 al 99» |
| Dorsal | que no lo lleve ya otro jugador del equipo | «Ese dorsal ya lo lleva otro jugador» |
En el nombre y en el dorsal gana el primer motivo que se cumple, en el orden de la tabla.
Piezas nuevas de este paso:
static let maximoNombre = 25: una constante del tipo, no de cada formulario. Dentro del tipo se lee conSelf.maximoNombre:Selfcon mayúscula es «este mismo tipo» (FormularioJugador), mientras queselfcon minúscula sería «este formulario en concreto».limpio.county(1...99).contains(numero): el recuento de caracteres y el rango, que acabas de usar en el ejercicio.plantilla.contains(where: { … }): «¿alguno de la lista cumple esto?».caseInsensitiveCompare(limpio) == .orderedSame: comparar dos textos sin distinguir mayúsculas de minúsculas. Viene de Foundation.dorsalDesdeTexto(_:), que escribiste en guard let y textos que no son números: quita los espacios de los bordes y devuelve el número, onilsi el texto no lo es. Por eso" 7"(con un espacio delante) da 7 sin que el formulario lo limpie antes.struct FormularioJugador: Equatable:Equatablees «se puede comparar con==». Los dosenumde errores lo necesitan para que las pruebas escriban== .vacio; en elstructes solo la costumbre de dejar que los datos se puedan comparar.
Intenta escribirlo tú: un struct FormularioJugador que se pueda comparar, con la constante del máximo, el nombre y el dorsal (textos que empiezan vacíos y marcan su campo como tocado al cambiar), los dos «tocado» (de solo lectura desde fuera), una función por campo que recibe la plantilla y devuelve el error o nil, esValido(en:), y aJugador(), que da el Jugador ya limpio. Y debajo, los dos enum de errores, que también se pueden comparar.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import Foundation
import Torneo
// EL FORMULARIO «AÑADIR JUGADOR», COMO DATOS (gemelo de `FormularioJugador.kt`).
//
// Un `struct`: lo que el usuario ESCRIBIÓ, tal cual, y qué campos ha TOCADO. Los errores no se
// guardan: se CALCULAN a partir del texto y de la plantilla guardada.
struct FormularioJugador: Equatable {
static let maximoNombre = 25
// `didSet` corre cada vez que se escribe en la propiedad; `oldValue` es el valor de antes. Solo
// si el texto cambió de verdad el campo queda tocado. Así el `TextField` puede escribir
// DIRECTAMENTE aquí (`$viewModel.formulario.nombre`).
var nombre = "" { didSet { if nombre != oldValue { nombreTocado = true } } }
var dorsal = "" { didSet { if dorsal != oldValue { dorsalTocado = true } } }
private(set) var nombreTocado = false
private(set) var dorsalTocado = false
func errorNombre(en plantilla: [Jugador]) -> ErrorNombre? {
let limpio = nombre.trimmingCharacters(in: .whitespaces)
if limpio.isEmpty { return .vacio }
if limpio.count > Self.maximoNombre { return .largo }
if plantilla.contains(where: { $0.nombre.caseInsensitiveCompare(limpio) == .orderedSame }) { return .repetido }
return nil
}
func errorDorsal(en plantilla: [Jugador]) -> ErrorDorsal? {
if dorsal.trimmingCharacters(in: .whitespaces).isEmpty { return nil } // opcional
guard let numero = dorsalDesdeTexto(dorsal) else { return .noEsNumero }
if !(1...99).contains(numero) { return .fueraDeRango }
if plantilla.contains(where: { $0.dorsal == numero }) { return .repetido }
return nil
}
func esValido(en plantilla: [Jugador]) -> Bool {
errorNombre(en: plantilla) == nil && errorDorsal(en: plantilla) == nil
}
func aJugador() -> Jugador {
Jugador(nombre: nombre.trimmingCharacters(in: .whitespaces), dorsal: dorsalDesdeTexto(dorsal))
}
}
// Un `enum` por campo; la vista elige el texto de cada caso.
enum ErrorNombre: Equatable { case vacio, largo, repetido }
enum ErrorDorsal: Equatable { case noEsNumero, fueraDeRango, repetido }Compila con Cmd+B: Build Succeeded. Fíjate en tres decisiones. Los errores reciben la plantilla por parámetro (en plantilla:) en lugar de guardarla: el formulario no sabe de repositorios, y se prueba con una lista cualquiera. aJugador() solo tiene sentido con el formulario válido: limpia los espacios y convierte el dorsal en número, o en nil si estaba vacío. Y didSet permite que el TextField escriba directamente en el struct ($viewModel.formulario.nombre, en la lección siguiente) y que el campo quede tocado sin ningún código más. La comparación con oldValue es necesaria porque el TextField puede volver a escribir el mismo texto en el campo cuando lo tocas para ponerte a escribir: tocar no es escribir, y sin la comparación el error rojo saldría nada más tocar el campo.
Paso 2 · El ViewModel: AnadirJugadorViewModel.swift
Objetivo: una clase que guarda el formulario, sabe cuál es la plantilla guardada de su equipo y decide qué errores se ven.
Créalo en Plantilla/UI, junto al formulario (clic derecho sobre esa carpeta → New File from Template… → Swift File, solo con el target MarcadorApp). Intenta escribirlo tú: una clase final, @MainActor y @Observable (como los otros ViewModel), con el nombre del equipo (let), el formulario (una var que empieza vacía) y el repositorio como @ObservationIgnored private let del tipo any PartidosRepositorio, recibidos por el init. Una propiedad calculada plantilla que busca el equipo por nombre en repositorio.equipos y devuelve su plantilla (o una lista vacía). Tres propiedades calculadas para la vista: errorNombre y errorDorsal, que dan el error solo si el campo está tocado, y sePuedeGuardar, que dice si el formulario entero es válido, tocado o no. Y guardar() -> Bool: si no se puede guardar, devuelve false; si se puede, pide al repositorio que añada el jugador y devuelve lo que este conteste.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import Foundation
import Observation
import Torneo
// El formulario vive en el ViewModel (`@Observable`): la vista escribe en `formulario` con un
// `Binding` y lee los errores, que se calculan cada vez con la plantilla GUARDADA.
@MainActor
@Observable
final class AnadirJugadorViewModel {
let equipo: String
var formulario = FormularioJugador()
@ObservationIgnored private let repositorio: any PartidosRepositorio
init(equipo: String, repositorio: any PartidosRepositorio) {
self.equipo = equipo
self.repositorio = repositorio
}
// Se lee del repositorio en cada consulta: si cambia, los errores cambian con él.
private var plantilla: [Jugador] {
repositorio.equipos.first { $0.nombre == equipo }?.plantilla ?? []
}
// Lo que ve la vista: el error SOLO si el campo está tocado.
var errorNombre: ErrorNombre? { formulario.nombreTocado ? formulario.errorNombre(en: plantilla) : nil }
var errorDorsal: ErrorDorsal? { formulario.dorsalTocado ? formulario.errorDorsal(en: plantilla) : nil }
var sePuedeGuardar: Bool { formulario.esValido(en: plantilla) }
// Devuelve si se guardó: la vista cierra la pantalla solo entonces.
func guardar() -> Bool {
guard sePuedeGuardar else { return false }
return repositorio.anadirJugador(formulario.aJugador(), a: equipo)
}
}Dos detalles. plantilla no es una propiedad guardada: es calculada, y lee repositorio.equipos cada vez que alguien le pregunta. Y no hay contradicción con @ObservationIgnored: lo que se ignora es la propiedad repositorio (la caja que guarda el repositorio, que nunca cambia de uno a otro), no lo que hay dentro; repositorio.equipos se sigue observando porque el repositorio es @Observable. (En un let la marca no cambia nada que veas; se pone para dejar claro que esa caja no forma parte de lo que se pinta, como en los demás ViewModel del proyecto.) Así, cuando se añada un jugador, los errores se recalculan solos: tras guardar a Bea con el 7, un segundo «Bea» o un segundo 7 ya dan error. Y sePuedeGuardar no mira si los campos están tocados: al abrir el formulario el botón estará apagado, pero sin errores en rojo. Así el botón dice «todavía no» sin gritar.
Paso 3 · Las pruebas de las reglas: FormularioJugadorTests.swift
Objetivo: siete pruebas de las reglas, sin vista ni ViewModel.
Las pruebas copian las carpetas de la app, y esta prueba es de Plantilla/UI. Clic derecho sobre MarcadorAppTests → New Folder, de nombre Plantilla; clic derecho sobre Plantilla → New Folder, de nombre UI (con la Terminal: mkdir -p ~/Marcador-Swift/MarcadorApp/MarcadorAppTests/Plantilla/UI). Crea FormularioJugadorTests.swift: clic derecho sobre MarcadorAppTests/Plantilla/UI → New File from Template… → Swift File, solo con el target MarcadorAppTests. Siete pruebas:
- Un nombre vacío o de solo espacios es
vacio. - Con 26 letras es
largo, y con 25 no hay error. - « ana » ya está en la plantilla del Rayo (Ana), así que es
repetido: sin distinguir mayúsculas y sin contar espacios. - El dorsal es opcional: sin escribirlo no hay error y el formulario es válido.
- Los dorsales que no valen, en una sola prueba parametrizada: «diez» es
noEsNumero, «0» y «100» sonfueraDeRangoy «9» esrepetido(Ana lo lleva). - Escribir en el nombre lo marca como tocado, y el dorsal sigue sin tocar.
- Un formulario válido da el jugador limpio:
" Bea "y" 7"danJugador(nombre: "Bea", dorsal: 7).
Ya la conoces, con dos detalles nuevos: @Test(arguments: [...]) ejecuta la misma prueba una vez por cada elemento de la lista, como en La regla de los puntos y tu primer test. Aquí cada elemento es una tupla de un texto y un error, y sus dos valores llegan a los parámetros de la función. En Xcode se ve una fila con cuatro casos dentro. Y en la lista, solo el primer elemento escribe el tipo completo, ErrorDorsal.noEsNumero; los demás pueden poner .fueraDeRango, porque Swift ya sabe que ese lado de la tupla es un ErrorDorsal. @MainActor delante de la struct de pruebas hace que corran en el hilo principal, como el ViewModel. Y String(repeating: "a", count: 26) fabrica un texto con la «a» repetida 26 veces, para no escribirlas a mano.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import Testing
import Torneo
@testable import MarcadorApp
// Las reglas del formulario, sin vista ni ViewModel. La plantilla del Rayo FC: Ana #9, Luis sin
// dorsal y Marta #1.
@MainActor
struct FormularioJugadorTests {
let rayo = Ejemplo.rayo.plantilla
func formulario(nombre: String = "", dorsal: String = "") -> FormularioJugador {
var formulario = FormularioJugador()
formulario.nombre = nombre
formulario.dorsal = dorsal
return formulario
}
@Test func unNombreVacioOSoloConEspaciosEsUnError() {
#expect(formulario(nombre: "").errorNombre(en: rayo) == .vacio)
#expect(formulario(nombre: " ").errorNombre(en: rayo) == .vacio)
}
@Test func unNombreDeMasDe25LetrasEsDemasiadoLargo() {
#expect(formulario(nombre: String(repeating: "a", count: 26)).errorNombre(en: rayo) == .largo)
#expect(formulario(nombre: String(repeating: "a", count: 25)).errorNombre(en: rayo) == nil)
}
@Test func unNombreQueYaEstaEnLaPlantillaEsUnError() {
#expect(formulario(nombre: " ana ").errorNombre(en: rayo) == .repetido)
}
@Test func elDorsalEsOpcional() {
#expect(formulario(nombre: "Bea").errorDorsal(en: rayo) == nil)
#expect(formulario(nombre: "Bea").esValido(en: rayo))
}
@Test(arguments: [("diez", ErrorDorsal.noEsNumero), ("0", .fueraDeRango), ("100", .fueraDeRango), ("9", .repetido)])
func losDorsalesQueNoValen(texto: String, error: ErrorDorsal) {
#expect(formulario(nombre: "Bea", dorsal: texto).errorDorsal(en: rayo) == error)
}
@Test func escribirMarcaElCampoComoTocado() {
var formulario = FormularioJugador()
#expect(!formulario.nombreTocado)
formulario.nombre = "B"
#expect(formulario.nombreTocado)
#expect(!formulario.dorsalTocado)
}
@Test func unFormularioValidoDaElJugadorLimpio() {
#expect(formulario(nombre: " Bea ", dorsal: " 7").aJugador() == Jugador(nombre: "Bea", dorsal: 7))
}
}Dos detalles del fichero de pruebas. La función auxiliar formulario(nombre:dorsal:) tiene valores por defecto (nombre: String = "": si no pasas ese argumento, vale vacío), y en la prueba escribirMarcaElCampoComoTocado la variable local formulario tapa a esa función mientras dura la prueba: dentro de ella formulario es la variable, no la función.
Lanza Cmd+U. Hay una cuenta que sorprende: son 7 pruebas nuevas, pero la parametrizada cuenta una en el recuento aunque ejecute cuatro casos, así que el total sube en 7, no en 10.
Punto de control
- Cmd+B termina en
Build Succeededy Cmd+U da 58 pruebas en verde: 42 unitarias y 16 de interfaz (con tus ejercicios de pruebas, 61). Las siete nuevas son las deFormularioJugadorTests; la parametrizada sale con 4 casos. - La app se ve y se comporta igual que antes: el formulario todavía no tiene pantalla.
- El ejercicio de la reserva de mesa imprime las siete líneas.
- Sabes decir por qué un error se calcula y no se guarda, qué quiere decir «campo tocado» y por qué el botón puede estar apagado sin que haya ningún error a la vista.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.¿Por qué los errores del formulario se calculan en vez de guardarse?
2.En
FormularioJugador, ¿qué significa que un campo esté «tocado»?3.Recién abierto el formulario, ¿qué dicen
sePuedeGuardary los errores visibles?4.En iOS, el
ViewModelno copia la plantilla guardada: la lee del repositorio en cada consulta. ¿Por qué eso basta para que los errores se actualicen?5.Alguien llama a
anadirJugadorcon un dorsal repetido sin pasar por el formulario. ¿Qué pasa?