Form, secciones y controles: el aspecto nativo de los ajustes
Form no es un contenedor de apilamiento con bordes bonitos: es un contexto de presentación que reescribe el aspecto y el comportamiento de todo lo que contiene, de modo que un Toggle se convierte en un interruptor alineado a la derecha y un Picker en una fila que empuja a otra pantalla. Esta lección examina esa negociación entre contenedor y control, recorre Toggle, Picker, Stepper y DatePicker atendiendo a la semántica del valor que cada uno modela, disecciona el fallo de emparejamiento de tipos que deja un Picker sin selección visible y argumenta por qué el aspecto de los Ajustes del sistema es un activo que conviene no derrochar.
Cuando metes un Toggle en un VStack obtienes una etiqueta y un interruptor; cuando lo metes en un Form obtienes una fila de Ajustes, con su alineación, su separador, su altura mínima táctil y su comportamiento al crecer el tipo de letra. No has cambiado el control: has cambiado el contexto que lo interpreta. Entender Form como un negociador de presentación —y no como decoración— es lo que permite escribir formularios que se sienten del sistema en iPhone, iPad y Mac sin escribir tres versiones.
- Estructurar un formulario con
Form,Sectiony encabezados y pies con propósito. - Elegir entre
Toggle,Picker,StepperyDatePickersegún la semántica del valor. - Diagnosticar el fallo de emparejamiento de tipos entre
tagy la selección de unPicker. - Justificar cuándo conservar el aspecto nativo y cuándo apartarse de él.
Form negocia, no dibuja
Form establece un estilo que sus descendientes consultan. Section agrupa filas y aporta dos huecos de texto que casi nadie aprovecha: el encabezado, que nombra el grupo, y el pie, que explica la consecuencia de lo que hay dentro. Ese pie es el lugar canónico de la documentación en una interfaz de ajustes.
Form {
Section("Perfil") {
TextField("Nombre visible", text: $nombre)
LabeledContent("Identificador", value: cuenta.id)
}
Section {
Toggle("Sincronizar en segundo plano", isOn: $sincroniza)
} footer: {
Text("La sincronización usa datos móviles cuando no hay red conocida.")
}
}
.formStyle(.grouped)
flowchart TB F[Form define el estilo] --> S1[Section con encabezado] F --> S2[Section con pie explicativo] S1 --> R1[Fila de texto] S1 --> R2[Fila de contenido etiquetado] S2 --> R3[Fila de interruptor] R3 --> P[El pie documenta la consecuencia]
LabeledContent merece un lugar propio: es la fila de etiqueta y valor no editable, con la alineación correcta y el comportamiento adecuado al crecer el texto. Escribirla a mano con un HStack y un Spacer produce algo parecido que se rompe en tipografías grandes y no se agrupa bien para el lector de pantalla.
La semántica del valor manda sobre el aspecto
Cada control existe porque modela una forma distinta de dato. Elegir por costumbre visual, y no por el tipo del valor, es el origen de la mayoría de los formularios incómodos.
Toggle
Un booleano cuyo cambio tiene efecto inmediato. Si el cambio solo se aplica al guardar, un interruptor miente sobre su propia inmediatez.
Picker
Una elección dentro de un conjunto conocido. .menu para pocas opciones, .navigationLink para muchas, .segmented cuando conviene ver todas a la vez.
Stepper
Un entero acotado que se ajusta en pasos pequeños. Con rango y paso explícitos evitas validar después lo que el control ya garantizaba.
DatePicker
Fecha, hora o ambas, con rango permitido. displayedComponents decide qué se pide y el rango decide qué es posible.
Section("Recordatorio") {
Picker("Prioridad", selection: $prioridad) {
ForEach(Prioridad.allCases) { p in
Text(p.titulo).tag(p)
}
}
Stepper("Repetir \(repeticiones) veces",
value: $repeticiones, in: 1...10)
DatePicker("Cuándo", selection: $fecha,
in: Date.now...,
displayedComponents: [.date, .hourAndMinute])
}
La selección se empareja con las etiquetas comparando el tipo exacto del tag. Si el binding es un valor opcional y las etiquetas llevan un tag no opcional, ningún elemento coincide y el control aparece sin selección, sin error de compilación y sin pista alguna. La solución es hacer coincidir los tipos de forma explícita, marcando el tag como opcional del mismo tipo, o eliminar la opcionalidad del estado si el dominio siempre tiene un valor por defecto.
El aspecto nativo es capital acumulado
Un usuario de iOS ha visto miles de filas de Ajustes. Sabe, sin leer, que el interruptor de la derecha se aplica al instante, que la fila con acento a la derecha lleva a otra pantalla, que el texto pequeño bajo el grupo explica lo que hace el grupo. Ese conocimiento previo es capital que tu aplicación hereda gratis en cuanto usas Form y Section tal como son.
Form {
Section {
Picker("Tema", selection: $tema) {
ForEach(Tema.allCases) { t in Text(t.titulo).tag(t) }
}
.pickerStyle(.navigationLink)
} header: {
Text("Apariencia")
} footer: {
Text("Automático sigue la configuración del sistema.")
}
}
Apartarse del estilo nativo es legítimo cuando el formulario es la experiencia central del producto y su marca lo exige, cuando un paso de incorporación debe sentirse distinto del resto, o cuando el dato tiene una forma que ninguna fila estándar representa bien. Es ilegítimo cuando la razón es que el diseño de escritorio no tenía filas agrupadas.
El mismo código en tres plataformas
formStyle es el punto donde se decide la disposición sin tocar el contenido. .grouped produce las tarjetas de Ajustes propias de iOS; .columns alinea etiquetas y controles en dos columnas, que es la convención de macOS; .automatic deja que el sistema elija según el contexto. Cambiar de estilo no reescribe una sola fila.
Form {
Section("Cuenta") {
TextField("Nombre", text: $nombre)
Picker("Zona horaria", selection: $zona) {
ForEach(Zona.allCases) { z in Text(z.titulo).tag(z) }
}
Toggle("Recibir avisos", isOn: $avisos)
}
}
#if os(macOS)
.formStyle(.columns)
#else
.formStyle(.grouped)
#endif
Los estilos de control también se adaptan solos: un Picker con estilo automático se presenta como menú en iOS y como desplegable en macOS, y un DatePicker ofrece la rueda compacta en el teléfono y el calendario completo cuando hay espacio. Forzar un estilo concreto con pickerStyle o datePickerStyle es renunciar a esa adaptación, y solo compensa cuando el número de opciones o la naturaleza del dato lo justifican en todas las plataformas a la vez.
Fijar la altura de una fila o la anchura de una etiqueta funciona hasta que el usuario sube el tamaño de letra o cambia de idioma, y entonces el texto se corta sin aviso. Deja que la fila se dimensione a partir de su contenido, usa LabeledContent para la alineación y reserva las medidas explícitas para lo que de verdad tiene tamaño intrínseco, como una imagen.
La cuestión de fondo de esta lección no es estética sino epistemológica: cuánto tiene que aprender el usuario para operar tu pantalla. Cada convención del sistema —el interruptor que se aplica al soltarlo, la fila que empuja hacia el detalle, el pie de sección que explica sin ocupar espacio, la altura mínima que garantiza el toque, el comportamiento al escalar la tipografía— es una regla que millones de personas ya interiorizaron usando el propio dispositivo, y que tu aplicación importa completa en el momento en que usa el componente tal como está. Cuando reimplementas una fila con un apilamiento y un separador dibujado a mano, no estás copiando el aspecto: estás renunciando a todo lo que había debajo, porque los estilos del sistema no son pintura sino un paquete indivisible de aspecto, semántica de accesibilidad, adaptación por plataforma y respuesta a los ajustes del usuario. La consecuencia práctica es una regla de decisión sencilla y exigente: aléjate del control nativo solo cuando puedas nombrar qué ganas y reponer lo que pierdes; y si al enumerar lo que pierdes aparecen palabras como VoiceOver, tipo dinámico, iPad o macOS, la respuesta ya está dada. La segunda consecuencia es más sutil: la elección del control comunica un compromiso. Un interruptor promete efecto inmediato, un Picker promete un conjunto cerrado y conocido, un Stepper promete que el valor es pequeño y acotado. Usar el control equivocado no es un fallo visual, es una afirmación falsa sobre cómo se comporta tu sistema.
- Toma una pantalla de configuración existente y reescríbela con
FormySection, sin apilamientos manuales ni separadores dibujados. - Añade un pie a cada sección que explique la consecuencia real de sus controles, y elimina cualquier texto de ayuda que sobre.
- Sustituye por
LabeledContenttodas las filas de etiqueta y valor hechas a mano, y comprueba el resultado con el tipo de letra más grande. - Provoca a propósito el fallo de emparejamiento entre la selección opcional y un
tagno opcional, observa elPickersin selección y arréglalo alineando los tipos. - Elige un control que hoy mienta sobre su semántica —un interruptor cuyo efecto solo llega al guardar— y decide si cambias el control o cambias el comportamiento.