O que é acessibilidade no iOS (e por que isso muda seu código)
Acessibilidade é o conjunto de práticas que permite que pessoas com diferentes necessidades (visuais, motoras, cognitivas e auditivas) usem seu app com autonomia. No iOS, três pilares aparecem com frequência no dia a dia de desenvolvimento com SwiftUI: (1) leitura e navegação por leitor de tela (VoiceOver), (2) adaptação de texto e layout ao tamanho de fonte do usuário (Dynamic Type) e (3) contraste e diferenciação visual adequada (incluindo Alto Contraste e “Diferenciar sem cor”).
Em SwiftUI, grande parte da acessibilidade vem “de graça” quando você usa componentes nativos corretamente. Mesmo assim, é comum precisar ajustar rótulos, dicas, valores, ordem de navegação e áreas de toque. Este capítulo foca no que você precisa aplicar no app do curso para que ele seja confortável com VoiceOver, escalável com Dynamic Type e legível em diferentes condições de contraste.
VoiceOver na prática: rótulos, dicas, valores e agrupamento
Como o VoiceOver “enxerga” sua interface
O VoiceOver percorre elementos acessíveis em uma ordem (geralmente de cima para baixo e da esquerda para a direita), anunciando: label (o que é), value (estado/valor atual) e, quando útil, um hint (o que acontece ao interagir). Se você não define nada, o sistema tenta inferir a partir de textos e ícones, mas isso falha em botões só com imagem, componentes customizados e layouts complexos.
Quando usar accessibilityLabel, accessibilityHint e accessibilityValue
accessibilityLabel: nome curto e claro do controle. Ex.: “Adicionar ao carrinho”.accessibilityHint: descreve a ação após tocar duas vezes. Ex.: “Abre a tela de pagamento”. Evite repetir o label.accessibilityValue: estado/valor atual. Ex.: “Ativado”, “3 itens”, “50%”.
Exemplo 1: botão com ícone (sem texto visível)
Botões com Image(systemName:) costumam ser lidos como o nome do SF Symbol (ruim) ou como nada (pior). Defina um label humano e, se necessário, um hint.
Button { viewModel.addItem() } label: { Image(systemName: "plus") } .accessibilityLabel("Adicionar item") .accessibilityHint("Cria um novo item na lista")Exemplo 2: indicador de status com valor acessível
Se você mostra um status visual (ex.: “Prioridade”, “Progresso”, “Favorito”), exponha o valor para o VoiceOver. Isso é especialmente importante quando o texto na tela é abreviado ou quando o status é representado por cor/ícone.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
HStack { Image(systemName: item.isFavorite ? "star.fill" : "star") Text(item.title) } .accessibilityElement(children: .combine) .accessibilityLabel("\(item.title)") .accessibilityValue(item.isFavorite ? "Favorito" : "Não favorito")Neste caso, .combine faz o VoiceOver ler o conjunto como um único elemento, evitando que ele pare no ícone e depois no texto separadamente.
Exemplo 3: controles customizados e ordem de leitura
Em layouts com vários elementos próximos (cards, células de lista customizadas), você pode querer: (a) combinar filhos em um único elemento, ou (b) controlar a ordem de navegação.
VStack(alignment: .leading, spacing: 8) { Text(item.title).font(.headline) Text(item.subtitle).font(.subheadline) HStack { Text(item.priceFormatted) Spacer() Button("Comprar") { viewModel.buy(item) } } } .accessibilityElement(children: .contain) .accessibilitySortPriority(1).contain mantém elementos separados, mas ainda “agrupa” a área. Já accessibilitySortPriority ajuda quando a ordem natural do layout não fica boa (use com parcimônia; prioridades altas demais podem confundir).
Quando esconder elementos decorativos
Ícones puramente decorativos devem ser ignorados pelo VoiceOver para reduzir ruído.
Image(systemName: "chevron.right") .accessibilityHidden(true)Tamanho de toque e áreas clicáveis (motor e precisão)
Um problema comum é ter botões visualmente pequenos, difíceis de tocar. O iOS recomenda alvos de toque confortáveis (em torno de 44x44 pt). Em SwiftUI, você pode aumentar a área tocável sem mudar o visual usando contentShape e padding.
Exemplo: aumentar área de toque de um ícone
Button { viewModel.toggleFavorite(item) } label: { Image(systemName: item.isFavorite ? "heart.fill" : "heart") .foregroundStyle(.red) } .padding(12) .contentShape(Rectangle()) .accessibilityLabel(item.isFavorite ? "Remover dos favoritos" : "Adicionar aos favoritos")O padding aumenta a área; contentShape(Rectangle()) garante que a região clicável seja retangular (útil quando o conteúdo é pequeno ou irregular).
Dynamic Type: texto que cresce sem quebrar o layout
O que é Dynamic Type
Dynamic Type é o sistema de tamanhos de fonte do iOS. Usuários podem aumentar (ou reduzir) o tamanho do texto nas configurações. Seu app deve acompanhar isso sem truncar informações importantes, sem sobrepor elementos e sem impedir a navegação.
Boas práticas essenciais no SwiftUI
- Prefira fontes semânticas:
.font(.body),.headline,.title3etc., em vez de tamanhos fixos. - Evite
.frame(height:)rígido em componentes com texto. - Permita múltiplas linhas quando fizer sentido:
.lineLimit(nil)(ou não definir) e.fixedSize(horizontal: false, vertical: true)em casos específicos. - Use
LayoutPrioritypara evitar que textos importantes sejam comprimidos.
Exemplo: card com título e subtítulo que respeita Dynamic Type
VStack(alignment: .leading, spacing: 6) { Text(item.title) .font(.headline) .lineLimit(2) .layoutPriority(1) Text(item.subtitle) .font(.body) .foregroundStyle(.secondary) } .padding() .background(.thinMaterial) .clipShape(RoundedRectangle(cornerRadius: 12))Aqui, layoutPriority(1) ajuda o título a “ganhar espaço” quando o layout apertar. Evite limitar demais linhas em conteúdos críticos (endereços, nomes, instruções).
Testando tamanhos de fonte no Preview
Use o Preview para simular tamanhos maiores e identificar quebras cedo.
#Preview("Acessibilidade - Dynamic Type") { SuaTela() .environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge) }Contraste, cores e “diferenciar sem cor”
O problema: informação só por cor
Se você indica estado apenas por cor (ex.: verde = ok, vermelho = erro), parte dos usuários pode não perceber. Sempre que possível, combine cor com texto, ícone ou forma.
Use cores semânticas e estilos do sistema
Prefira .foregroundStyle(.primary), .secondary e cores do sistema (como .tint) em vez de cores fixas. Isso melhora contraste automaticamente em diferentes modos (claro/escuro) e configurações de acessibilidade.
Exemplo: mensagem de erro com ícone + texto (não só vermelho)
HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") Text("E-mail inválido") } .font(.callout) .foregroundStyle(.red) .accessibilityElement(children: .combine) .accessibilityLabel("Erro: e-mail inválido")Mesmo usando vermelho, o ícone e o texto tornam o significado claro. O accessibilityLabel deixa a leitura objetiva.
Detectando configurações do usuário (alto contraste e diferenciar sem cor)
Você pode reagir a preferências do usuário com variáveis de ambiente. Use isso para reforçar bordas, aumentar separação visual e evitar depender só de cor.
@Environment(\.accessibilityDifferentiateWithoutColor) private var differentiateWithoutColor @Environment(\.accessibilityContrast) private var accessibilityContrastvar body: some View { let needsExtraContrast = (accessibilityContrast == .increased) VStack { Text("Status") .padding() } .background(needsExtraContrast ? Color.black.opacity(0.08) : Color.clear) .overlay { if differentiateWithoutColor { RoundedRectangle(cornerRadius: 10).stroke(.primary, lineWidth: 2) } } }Esse padrão cria uma “pista” visual adicional (borda) quando o usuário pede para diferenciar sem cor, e reforça contraste quando necessário.
Identificadores de acessibilidade (e por que ajudam em testes)
accessibilityIdentifier não é lido pelo VoiceOver, mas é essencial para testes de UI (XCUITest) encontrarem elementos de forma estável. Também ajuda em depuração quando você inspeciona a hierarquia de acessibilidade.
Exemplo: definindo identifiers em campos e botões
TextField("E-mail", text: $email) .textInputAutocapitalization(.never) .keyboardType(.emailAddress) .accessibilityIdentifier("login.email") Button("Entrar") { viewModel.login() } .accessibilityIdentifier("login.submit")Use um padrão consistente, por exemplo: tela.elemento ou fluxo.tela.elemento.
Passo a passo: revisão de acessibilidade no app do curso
A seguir está um roteiro prático para você aplicar agora no app do curso. A ideia é abrir a tela principal do app e corrigir problemas comuns em sequência.
Etapa 1 — Mapear elementos interativos e garantir labels corretos
- Liste todos os elementos tocáveis: botões, toggles, links, células clicáveis, ícones com ação.
- Para cada um, confirme: o texto visível já descreve a ação? Se não, adicione
accessibilityLabel. - Se a ação não for óbvia, adicione
accessibilityHint(curto e direto).
// Exemplo típico: botão de filtro só com ícone Button { viewModel.openFilters() } label: { Image(systemName: "line.3.horizontal.decrease.circle") } .accessibilityLabel("Filtros") .accessibilityHint("Abre opções para filtrar a lista") .accessibilityIdentifier("home.filters")Etapa 2 — Corrigir células/linhas “barulhentas” (muitos stops do VoiceOver)
- Se uma célula tem ícone + título + subtítulo + badge, o VoiceOver pode parar em cada item.
- Decida: a célula deve ser lida como um único elemento? Use
.accessibilityElement(children: .combine). - Se precisa manter elementos separados, use
.containe garanta que cada parte tenha sentido sozinha.
// Célula que deve ser lida como uma frase única HStack(spacing: 12) { Image(systemName: "doc.text") Text(item.title) Spacer() Text(item.dateFormatted).foregroundStyle(.secondary) } .accessibilityElement(children: .combine) .accessibilityLabel("\(item.title), \(item.dateFormatted)")Etapa 3 — Verificar ordem de navegação e foco
- Abra a tela e percorra com VoiceOver: a ordem faz sentido?
- Se um elemento “pula” para fora de contexto, avalie reorganizar a hierarquia de views (primeira opção).
- Se não for possível, use
accessibilitySortPrioritypara ajustar pontos específicos.
// Exemplo: garantir que um alerta/aviso seja lido antes de ações Text("Pagamento pendente") .accessibilitySortPriority(2) Button("Pagar agora") { viewModel.pay() } .accessibilitySortPriority(1)Etapa 4 — Garantir alvo de toque confortável
- Procure ícones pequenos (ex.: lixeira, editar, favorito) e aumente a área com
padding. - Use
contentShapequando o layout tiver espaços vazios que deveriam ser clicáveis.
Button { viewModel.delete(item) } label: { Image(systemName: "trash") } .padding(12) .contentShape(Rectangle()) .accessibilityLabel("Excluir") .accessibilityHint("Remove este item")Etapa 5 — Ajustar para Dynamic Type (sem truncar conteúdo importante)
- No Preview, teste
.accessibilityExtraExtraExtraLarge. - Remova alturas fixas em cards/linhas com texto.
- Evite
lineLimit(1)em títulos importantes; prefira 2+ linhas quando necessário. - Se um texto some por falta de espaço, aumente
layoutPrioritydo que é mais importante.
#Preview("Tela - Fonte grande") { SuaTela() .environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge) }Etapa 6 — Reforçar contraste e não depender só de cor
- Substitua cores fixas por estilos semânticos quando possível.
- Para estados (erro/sucesso/selecionado), adicione texto/ícone.
- Use
accessibilityDifferentiateWithoutColorpara adicionar bordas/ícones quando necessário.
@Environment(\.accessibilityDifferentiateWithoutColor) private var diffWithoutColor var statusView: some View { HStack(spacing: 8) { Circle().fill(.green).frame(width: 10, height: 10) Text("Sincronizado") } .overlay(alignment: .leading) { if diffWithoutColor { Image(systemName: "checkmark.circle").offset(x: -2) } } .accessibilityElement(children: .combine) .accessibilityLabel("Status") .accessibilityValue("Sincronizado") }Checklist prático (para usar antes de enviar para revisão)
| Item | Verificação | Como corrigir |
|---|---|---|
| Botões com ícone | VoiceOver anuncia algo útil? | accessibilityLabel e, se necessário, accessibilityHint |
| Estados e contagens | Usuário entende o estado atual? | accessibilityValue (ex.: “Ativado”, “3 itens”) |
| Elementos decorativos | VoiceOver está “falando demais”? | accessibilityHidden(true) |
| Células complexas | Muitos stops por linha? | .accessibilityElement(children: .combine) ou .contain |
| Ordem de navegação | Fluxo de leitura faz sentido? | Reorganizar layout; em último caso accessibilitySortPriority |
| Alvo de toque | Controles pequenos são fáceis de tocar? | padding + contentShape |
| Dynamic Type | Fonte grande quebra layout? | Fontes semânticas, remover alturas fixas, ajustar lineLimit/layoutPriority |
| Contraste | Legível no modo escuro e alto contraste? | Usar estilos semânticos, reforçar bordas/ícones, evitar cor como único sinal |
| Testes de UI | Elementos são encontrados de forma estável? | accessibilityIdentifier com padrão consistente |