12k
All articles

Detecção Moderna de Recursos no iOS (Sem Sofrimento)

Detecção moderna de recursos no iOS sem dor: CSS.supports, testes de comportamento e pouco UA sniffing para iOS 26 e iPads antigos.

OpenReplay Team
OpenReplay Team
Detecção Moderna de Recursos no iOS (Sem Sofrimento)

Faça detecção de recursos por padrão: teste a capacidade que você realmente precisa com 'IntersectionObserver' in window ou CSS.supports('property', 'value'), e reserve as verificações de user-agent para o punhado de casos do iOS que genuinamente não podem ser detectados por recurso.

Quem já manteve uma tabela de números de versão do iOS em um codebase conhece a rotina: a Apple lança uma atualização, a tabela fica desatualizada e algo quebra em produção antes que alguém abra um ticket. Verificações de capacidade eliminam esse ciclo por completo.

A detecção de recursos resolve a maior parte das dores do Safari e do iOS: você para de manter tabelas de versão frágeis e passa a perguntar ao navegador o que ele consegue fazer. A parte difícil é o resíduo: um pequeno conjunto de casos extremos do iOS (iPads antigos que se reportam como macOS desktop, o congelamento do user-agent no iOS 26) onde não existe verificação de capacidade e você precisa fazer sniffing com cuidado. Este artigo apresenta o manual moderno para ambos.

Principais Conclusões

  • Detecte capacidades diretamente com 'x' in window, CSS.supports() e optional chaining. A MDN chama isso de “uma estratégia muito mais confiável” do que fazer parsing da string de user-agent.
  • Presença não é prova: o Safari pode reportar @supports como verdadeiro para um recurso que ele não aplica de fato, então, para os “mentirosos” conhecidos, renderize o elemento fora da tela e meça-o com getBoundingClientRect().
  • Como todos os navegadores no iOS rodam sobre o WebKit e a versão do Safari acompanha a versão principal do sistema operacional, uma verificação isMobileWebKit() somada a um gate com CSS.supports() infere a versão do iOS sem qualquer parsing de UA.
  • No iOS 26+, o Safari congela o token do sistema operacional em seu UA em um valor anterior ao 26 que, por sua vez, foi mudando (18_618_6_218_7). Nunca faça hardcode desse valor; em vez disso, faça o parsing do token Version/.
  • O congelamento é um comportamento do próprio Safari, documentado para iOS e iPadOS 26; Chrome e Firefox no iOS ainda reportam a versão real do iOS.

Detecção de recursos é o padrão

Teste a capacidade, não o navegador. Uma verificação de recurso se adapta automaticamente quando a Apple lança uma atualização, não exige tabela de manutenção e funciona de forma idêntica entre engines, que é exatamente o motivo pelo qual Apple e MDN a recomendam em vez do sniffing de user-agent. Você tem três ferramentas para o trabalho.

Para APIs JavaScript, verifique o objeto global ou use optional chaining:

if ("IntersectionObserver" in window) {
  // wire up lazy-loading
}
navigator.share?.({ title: "Modern feature detection" });

Para CSS, use CSS.supports() no JS ou a at-rule @supports na sua folha de estilos:

@supports (text-wrap-style: stable) {
  h1 { text-wrap-style: balance; }
}

Observe que navigator.userAgentData não é um fallback aqui: é exclusivo do Chromium e marcado como experimental, então Safari e Firefox não o implementam. Nunca é substituto para detecção de recursos quando seu alvo é o Safari no iOS.

Quando a detecção de recursos mente: presença não é prova

A detecção de recursos tem dois modos de falha que vale a pena nomear, porque a correção é diferente em cada caso. O primeiro é um antipadrão: detectar um Recurso B não relacionado para inferir o Recurso A. Como o A Beautiful Site documenta, no momento em que o navegador entrega um recurso antes do outro, sua verificação quebra silenciosamente. Não acople verificações a proxies.

O segundo é mais sutil: presença não é prova. O Safari pode reportar @supports como true para um valor que ele não aplica de fato, como a Evil Martians descobriu com a palavra-chave de alinhamento CSS safe. Para esses “mentirosos” conhecidos, não confie na flag de suporte. Renderize o elemento fora da tela e meça o resultado real com getBoundingClientRect():

const supportsSafeAlign = () => {
  const box = document.createElement("div");
  const child = document.createElement("span");
  child.textContent = "measure me";
  Object.assign(box.style, {
    display: "flex",
    justifyContent: "safe center",
    width: "5%",
    position: "absolute",
    top: "-9999px",
    left: "-9999px",
  });
  box.appendChild(child);
  document.body.appendChild(box);
  const applied = child.getBoundingClientRect().left >= box.getBoundingClientRect().left;
  document.body.removeChild(box);
  return applied;
};

Um teste comportamental não precisa de nenhuma afirmação sobre versão para estar correto: ele observa o que realmente foi renderizado. Essa é a classe de bug que o session replay é bom em revelar: um recurso condicionado que passa em sua verificação de suporte no ambiente de teste, mas se comporta mal silenciosamente na build de iOS de um usuário real.

Detectando versões do iOS com detecção de recursos

Como todo navegador no iOS roda sobre o WebKit e a versão do Safari está atrelada à versão principal do sistema operacional, uma verificação isMobileWebKit() somada a um gate com CSS.supports() para uma propriedade introduzida em um release conhecido do Safari permite inferir a versão do iOS sem fazer parsing de string de user-agent alguma. A heurística de mobile-WebKit se baseia em um evento de gesto que o WebKit expõe:

const isMobileWebKit = () => "ongesturechange" in window;

Para o gate de versão, consulte o primeiro release com suporte a uma propriedade nas notas de lançamento do Safari da Apple ou nos dados de compatibilidade da MDN, e então teste-a. A longhand text-wrap-style chegou no Safari 17.5, que entregou seus valores balance, stable e auto de uma vez, então qualquer um desses valores serve de gate limpo para iOS 17.5+:

const isAtLeastIOS175 = () =>
  window.CSS?.supports("text-wrap-style", "stable") ?? false;

Verifique você mesmo o mapeamento propriedade↔release antes de colocar em produção. As notas de lançamento ocasionalmente omitem mudanças, e uma flag de “suportado” pode ser mentirosa (veja a seção anterior). Trate isMobileWebKit() como uma heurística forte, não uma garantia de especificação: a Apple permite engines de navegador alternativas na UE no iOS 17.4+, então “iOS significa WebKit” é esmagadoramente verdadeiro, mas não absoluto.

Quando o sniffing de user-agent é o último recurso, com escopo limitado

Recorra ao user-agent apenas quando duas condições forem verdadeiras ao mesmo tempo. A capacidade genuinamente não tem teste de recurso, e um palpite errado não custa nada pior do que uma falha cosmética. Dois casos do iOS atendem a esse critério.

iPads antigos. A detecção de recursos não consegue separar um iPad de um Mac, porque desde o iPadOS 13 o user-agent padrão de um iPad é a mesma string que um Mac envia. Combine os sinais em vez disso. Um user-agent que se lê como Safari no macOS desktop, somado a uma verificação positiva de mobile-WebKit e a uma contagem de pontos de toque diferente de zero, significa um iPad vestido de Mac. O teste de pontos de toque importa: Macs reportam navigator.maxTouchPoints como 0, então ele impede que um Mac real corresponda mesmo que o sinal de evento de gesto esteja presente no Safari desktop.

const looksLikeMacSafari = /Macintosh/.test(navigator.userAgent);
const isIPad = () =>
  looksLikeMacSafari && isMobileWebKit() && navigator.maxTouchPoints > 0;

O congelamento do UA no iOS 26. No iOS 26 e iPadOS 26, o Safari parou de colocar a versão do sistema operacional em execução em seu user-agent e fixou o token em um release anterior. Esse valor fixado, por sua vez, foi mudando ao longo dos point releases (18_6 no lançamento, depois 18_6_2 no Safari 26.1, então 18_7 a partir do iOS 26.2), que é exatamente o motivo pelo qual você nunca deve fazer hardcode dele. Faça o parsing do token Version/, que ainda reflete a versão principal real do Safari (e, portanto, do iOS):

const iosMajor = () => {
  const m = navigator.userAgent.match(/Version\/(\d+)/);
  return m ? Number(m[1]) : null; // 26 on iOS 26.x Safari
};

Duas coisas refinam isso. Primeiro, o congelamento é exclusivo do Safari: análises independentes de logs de servidor do AppleInsider e da Lapcat Software confirmam que Chrome e Firefox no iOS ainda reportam a versão real do sistema operacional, então “não dá para detectar o iOS 26” é um problema específico do Safari, não de todo o iOS. Segundo, se você preferir não assumir o parsing, o ua-parser-js está na versão 2.0.10 em seu último release no npm, então fixe 2.0.10+. O pacote também tem um histórico documentado de incidentes de supply chain, então verifique o que você instala.

O manual

O fluxo completo é curto e roda nesta ordem:

  1. Detecte o recurso primeiro. 'x' in window, CSS.supports(), @supports, optional chaining. Isso cobre a esmagadora maioria das decisões de gating.
  2. Teste comportamentalmente os mentirosos. Quando não se pode confiar em uma flag de suporte, renderize fora da tela e meça com getBoundingClientRect().
  3. Infira a versão do iOS sem UA. isMobileWebKit() somado a um gate com CSS.supports() em uma propriedade confirmada nas notas de lançamento.
  4. Faça sniffing apenas nas bordas indetectáveis. O combinador de iPad antigo e o token Version/ para o congelamento do iOS 26. Use-os somente onde um sniff que falha não perde nenhuma funcionalidade.
  5. Teste em dispositivos reais e simuladores. Tanto as notas de lançamento quanto as flags de suporte deixam lacunas, e só o hardware real resolve a questão.

A detecção de recursos é o padrão porque sobrevive a atualizações que você não planejou; o sniffing de user-agent é a exceção de escopo limitado para os dois ou três casos do iOS que a plataforma torna genuinamente indetectáveis. Encadeie as verificações nessa ordem, fixe seu parsing do token Version/ em vez de qualquer string congelada do sistema operacional, e confirme o resultado em um iPad antigo real antes de colocar em produção.

Perguntas Frequentes

É possível detectar o iOS 26 em JavaScript apesar do congelamento do user-agent?

Sim. A partir do iOS 26, o Safari congela o token do sistema operacional em seu user-agent em um valor anterior ao 26 que foi mudando ao longo dos point releases (18_6, depois 18_6_2, então 18_7 a partir do iOS 26.2), então fazer hardcode dele falha. Em vez disso, faça o parsing do token Version/, que ainda reporta a versão principal real do Safari e, portanto, a versão principal do iOS. No Safari do iOS 26.x, um match de Version/(\\d+) retorna 26.

O congelamento do user-agent no iOS 26 afeta Chrome e Firefox no iOS?

Não. A Apple documenta o token congelado do sistema operacional como um comportamento do próprio Safari no iOS e iPadOS 26. Análises independentes de logs de servidor do AppleInsider e da Lapcat Software confirmam que Chrome e Firefox no iOS ainda reportam a versão real do sistema operacional em suas strings de user-agent, mesmo que todo navegador no iOS rode sobre o WebKit. Portanto, 'não dá para detectar o iOS 26' é um problema específico do Safari, não de todo o iOS.

Como diferenciar um iPad de um Mac no navegador?

Combine dois sinais, porque a detecção de recursos sozinha não consegue separá-los: desde o iPadOS 13, o user-agent padrão de um iPad é a mesma string que um Mac envia. Se o user-agent parece Safari no macOS desktop, mas uma verificação de mobile-WebKit indica que você está em um dispositivo com toque, você tem um iPad se passando por um Mac. Como eventos de gesto também aparecem no Safari desktop, condicione a navigator.maxTouchPoints maior que 0, já que Macs reportam 0.

navigator.userAgentData funciona no Safari?

Não. navigator.userAgentData é exclusivo do Chromium e marcado como experimental na MDN, então Safari e Firefox não o implementam. Nunca é um substituto viável para detecção de recursos quando seu alvo é o Safari no iOS. Teste capacidades diretamente com 'x' in window ou CSS.supports(), que a MDN avalia como uma abordagem muito mais confiável do que ler o user-agent.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.