Lección 3 de 10 · 11 min de lectura · Revisado el 26/10/2026
iOS: VoiceOver, Dynamic Type, API de accesibilidad de UIKit y SwiftUI, Voice Control
iOS tiene una de las pilas de accesibilidad más maduras del mercado, y los componentes estándar de UIKit y SwiftUI son accesibles por defecto. Los problemas aparecen con vistas personalizadas, imágenes como botones, agrupaciones incorrectas y tamaños fijos. Esta lección recorre las herramientas del usuario (VoiceOver, Dynamic Type, Voice Control) y las API con las que el desarrollador describe la interfaz.
VoiceOver en 3 minutos para desarrolladores
Activa VoiceOver con triple pulsación del botón lateral (configúralo en Ajustes > Accesibilidad > Atajo de accesibilidad) o pidiéndoselo a Siri. Gestos esenciales: un toque para seleccionar y oír, deslizar a derecha/izquierda para ir al siguiente/anterior elemento, doble toque para activar, tres dedos para desplazar, dos dedos en «Z» para volver atrás, rotor (girar dos dedos) para cambiar entre encabezados, enlaces, controles de formulario, etc. Doble toque y mantener permite arrastrar. Lo que VoiceOver lee de cada elemento, en orden: etiqueta (label), valor, rasgos (traits: botón, encabezado, seleccionado, ajustable…) y pista (hint). Si el elemento tiene acciones personalizadas, se anuncia «acciones disponibles» y se recorren deslizando arriba/abajo.
La API de UIAccessibility en UIKit
Cada UIView expone propiedades que alimentan el árbol de accesibilidad:
isAccessibilityElement: si la vista es un elemento (true por defecto en controles y etiquetas; false en vistas contenedoras).accessibilityLabel: el nombre. Breve, sin incluir el tipo («Enviar», no «Botón enviar»).accessibilityValue: el valor actual (el porcentaje de un deslizador, el texto de un campo, «3 de 5»).accessibilityTraits:.button,.link,.header,.image,.selected,.notEnabled,.adjustable,.updatesFrequently…accessibilityHint: qué ocurrirá («Abre el detalle del movimiento»). Opcional y el usuario puede desactivar las pistas.accessibilityElementsen un contenedor para definir el orden de los hijos;shouldGroupAccessibilityChildrenpara que VoiceOver los lea juntos.accessibilityCustomActions: acciones alternativas a gestos (eliminar, archivar, mover).accessibilityUserInputLabels: nombres alternativos para Voice Control.
Ejemplo: una celda de movimiento bancario compuesta por tres etiquetas y un icono. Sin intervención, VoiceOver lee cuatro elementos separados («Supermercado», «−42,30», «15 de marzo», «imagen»). Lo correcto es agruparlos en un solo elemento con etiqueta compuesta y acciones:
final class MovimientoCell: UITableViewCell {
func configurar(con m: Movimiento) {
conceptoLabel.text = m.concepto
importeLabel.text = m.importeFormateado
fechaLabel.text = m.fechaFormateada
iconoView.image = m.icono
// Un solo elemento accesible por celda
isAccessibilityElement = true
accessibilityLabel = "\(m.concepto), \(m.importeAccesible), \(m.fechaLarga)"
accessibilityTraits = .button
accessibilityHint = "Abre el detalle del movimiento"
accessibilityCustomActions = [
UIAccessibilityCustomAction(name: "Categorizar") { _ in
self.delegate?.categorizar(m); return true
},
UIAccessibilityCustomAction(name: "Compartir") { _ in
self.delegate?.compartir(m); return true
}
]
// El icono es decorativo
iconoView.isAccessibilityElement = false
}
}Fíjate en importeAccesible: «−42,30 €» se lee mejor como «menos cuarenta y dos euros con treinta céntimos» o al menos «−42,30 euros»; el símbolo puede leerse mal según la voz. Para los botones de icono, asigna siempre accessibilityLabel («Buscar», «Menú») o VoiceOver leerá el nombre del fichero («ic_search_24»).
Anuncios y cambios de pantalla
Para notificar a VoiceOver usa UIAccessibility.post(notification:argument:): .announcement con un texto para mensajes de estado (4.1.3), .layoutChanged con la vista que debe recibir el foco cuando cambia parte de la pantalla, y .screenChanged cuando aparece una pantalla o modal nuevo. En SwiftUI, AccessibilityNotification.Announcement("Guardado").post() desde iOS 17.
SwiftUI
SwiftUI deriva mucha accesibilidad de la declaración (un Button con texto ya tiene nombre y rol). Modificadores clave:
struct FilaMovimiento: View {
let m: Movimiento
var body: some View {
HStack {
Image(systemName: m.simbolo)
.accessibilityHidden(true) // decorativo
VStack(alignment: .leading) {
Text(m.concepto).font(.body)
Text(m.fechaFormateada).font(.caption)
}
Spacer()
Text(m.importeFormateado)
.font(.body.monospacedDigit())
}
.accessibilityElement(children: .combine) // un solo elemento
.accessibilityLabel("\(m.concepto), \(m.importeAccesible), \(m.fechaLarga)")
.accessibilityAddTraits(.isButton)
.accessibilityAction(named: "Categorizar") { categorizar(m) }
.accessibilityAction(named: "Compartir") { compartir(m) }
}
}
// Encabezado de sección
Text("Movimientos de marzo")
.font(.title2)
.accessibilityAddTraits(.isHeader)
// Imagen informativa
Image("grafico-gastos")
.accessibilityLabel("Gastos por categoría: alimentación 40 %, transporte 25 %, ocio 20 %, otros 15 %")Otros modificadores útiles: .accessibilityValue para estados («Activado»), .accessibilityAdjustableAction para controles tipo deslizador personalizados, .accessibilitySortPriority para el orden dentro de un grupo, @AccessibilityFocusState para mover el foco de VoiceOver (por ejemplo, al primer error de un formulario), .accessibilityInputLabels para Voice Control, y .accessibilityRepresentation para que una vista personalizada se comporte como un control estándar.
Error frecuente
Botones hechos con Image + .onTapGesture en SwiftUI o con UIImageView + UITapGestureRecognizer en UIKit. VoiceOver los anuncia como «imagen», sin rasgo de botón, y a veces ni siquiera son enfocables. Usa Button o UIButton: heredan rol, foco, activación por doble toque y Voice Control sin esfuerzo.
Dynamic Type
Dynamic Type es el sistema de escalado de texto de iOS: el usuario elige entre 12 tamaños (7 estándar y 5 de accesibilidad). La app lo respeta si:
- En UIKit usa
UIFont.preferredFont(forTextStyle: .body)(oUIFontMetricspara fuentes personalizadas) y activaadjustsFontForContentSizeCategory = trueen etiquetas, botones y campos. - En SwiftUI usa los estilos (
.font(.body),.headline…); escalan automáticamente. Para medidas asociadas al texto (iconos, espaciados) usa@ScaledMetric. - Las etiquetas tienen
numberOfLines = 0y las restricciones permiten crecer; en SwiftUI evita.lineLimit(1)y alturas fijas. - En tamaños de accesibilidad grandes, cambia la disposición:
@Environment(\.dynamicTypeSize)permite pasar deHStackaVStackcuandodynamicTypeSize.isAccessibilitySize. En UIKit, consultatraitCollection.preferredContentSizeCategory.isAccessibilityCategory. - Las barras de pestañas y de navegación muestran versiones ampliadas al pulsar largo si se usan los componentes estándar.
Prueba con el tamaño «AX5» y con «Texto en negrita» activado: es la prueba de fuego de 1.4.4.
Voice Control y Full Keyboard Access
Voice Control (Ajustes > Accesibilidad) permite controlar iOS por voz: «Pulsar Enviar», «Mostrar números», «Mostrar nombres». Funciona con el accessibilityLabel: si el botón de icono no tiene etiqueta, el usuario no puede nombrarlo. Si la etiqueta visible es larga, añade accessibilityUserInputLabels = ["Enviar", "Mandar"] para ofrecer alternativas cortas. Full Keyboard Access permite usar un teclado Bluetooth con Tab; los controles estándar lo soportan, las vistas personalizadas necesitan canBecomeFocused y gestión de UIFocusEnvironment. Switch Control recorre los elementos accesibles en orden, por lo que el orden y la agrupación bien hechos también le benefician.
Otros ajustes del sistema que la app debe respetar
UIAccessibility.isReduceMotionEnabled/@Environment(\.accessibilityReduceMotion): desactiva animaciones no esenciales.isReduceTransparencyEnabled,isDarkerSystemColorsEnabled(Aumentar contraste),isBoldTextEnabled: usa colores del sistema o define variantes de alto contraste en el catálogo de colores.buttonShapesEnabled: muestra subrayado o borde en botones de solo texto si está activado.isVoiceOverRunning: útil para ajustar comportamientos (por ejemplo, no autoavanzar un carrusel), nunca para ofrecer una «versión» distinta de la app.
Ejemplo
Formulario de alta: al pulsar «Continuar» con un campo vacío, la app marca el campo en rojo. Para VoiceOver añade: mensaje de error como texto visible asociado al campo (accessibilityLabel del campo incluye «Error: el NIF es obligatorio» o se enlaza con accessibilityValue), un anuncio «Hay 1 error en el formulario» con .announcement, y mueve el foco al primer campo con error mediante @AccessibilityFocusState. Así se cumplen 3.3.1, 4.1.3 y 2.4.3.
Resumen
- VoiceOver lee etiqueta, valor, rasgos y pista; el desarrollador los define con
accessibilityLabel,accessibilityValue,accessibilityTraitsyaccessibilityHint(o sus modificadores SwiftUI). - Agrupa las celdas compuestas en un elemento (
isAccessibilityElement/.accessibilityElement(children: .combine)) y expón las acciones con custom actions. - Usa
Button/UIButtonpara botones, marca encabezados con.header, oculta lo decorativo y anuncia cambios con notificaciones de accesibilidad. - Dynamic Type: fuentes preferidas,
adjustsFontForContentSizeCategory, sin líneas ni alturas fijas, disposición alternativa en tamaños de accesibilidad. - Voice Control depende de las etiquetas; respeta Reducir movimiento, Aumentar contraste y Texto en negrita.
Comprueba lo que has aprendido
Esta lección se puede leer libremente. Para hacer su test de 3 preguntas, los ejercicios prácticos con páginas reales, guardar tu avance y optar al examen y al certificado, matricúlate en el curso.
Matricularme · nivel 6 (119 €)