12k
All articles

Современное определение возможностей в iOS (без боли)

Современное определение функций iOS без боли: CSS.supports, поведенческие проверки и минимальный UA-sniffing для iOS 26 и старых iPad.

OpenReplay Team
OpenReplay Team
Современное определение возможностей в iOS (без боли)

По умолчанию используйте feature detection: проверяйте именно ту возможность, которая вам нужна, с помощью 'IntersectionObserver' in window или CSS.supports('property', 'value'), а проверки user-agent оставьте для тех немногих iOS-случаев, которые действительно невозможно определить через feature detection.

Любой, кто когда-либо держал в кодовой базе таблицу версий iOS, знает этот сценарий: Apple выпускает обновление, таблица устаревает, и что-то ломается в продакшене раньше, чем кто-либо успеет завести тикет. Проверки возможностей позволяют полностью избежать этого цикла.

Feature detection решает большинство проблем с Safari и iOS: вы перестаёте поддерживать хрупкие таблицы версий и начинаете спрашивать у браузера, что он умеет. Сложность — в остатке: небольшой набор пограничных случаев iOS (старые iPad, которые представляются как десктопная macOS, заморозка user-agent в iOS 26), где никакой проверки возможностей не существует и приходится аккуратно заниматься сниффингом. В этой статье собран современный подход к обоим сценариям.

Ключевые выводы

  • Определяйте возможности напрямую с помощью 'x' in window, CSS.supports() и опциональной цепочки. MDN называет это «гораздо более надёжной стратегией», чем разбор строки user-agent.
  • Наличие ещё не доказательство: Safari может сообщать через @supports, что возможность поддерживается, хотя фактически её не применяет, поэтому для известных «лжецов» рендерите элемент за пределами экрана и измеряйте его через getBoundingClientRect().
  • Поскольку все браузеры на iOS работают на WebKit, а версия Safari привязана к мажорной версии ОС, проверка isMobileWebKit() вместе с гейтом на CSS.supports() позволяет вывести версию iOS вообще без разбора UA.
  • В iOS 26+ Safari замораживает токен ОС в своём UA на значении из версий до 26, которое само по себе менялось (18_618_6_218_7). Никогда не хардкодьте его — вместо этого парсите токен Version/.
  • Заморозка — это собственное поведение Safari, задокументированное для iOS и iPadOS 26; Chrome и Firefox на iOS по-прежнему сообщают реальную версию iOS.

Feature detection — это подход по умолчанию

Проверяйте возможность, а не браузер. Проверка возможности автоматически подстраивается, когда Apple выпускает обновление, не требует таблицы для поддержки и работает одинаково во всех движках — именно поэтому Apple и MDN рекомендуют её вместо сниффинга user-agent. Для этой задачи у вас есть три инструмента.

Для JavaScript API проверяйте глобальный объект или используйте опциональную цепочку:

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

Для CSS используйте CSS.supports() в JS или at-правило @supports в таблице стилей:

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

Обратите внимание, что navigator.userAgentData здесь не выручит: это Chromium-only API с пометкой «экспериментальное», поэтому Safari и Firefox его не реализуют. Он никогда не заменяет feature detection, если ваша цель — iOS Safari.

Когда feature detection лжёт: наличие не равно поддержке

У feature detection есть два режима отказа, которые стоит назвать по именам, потому что исправляются они по-разному. Первый — это антипаттерн: определять не связанную возможность B, чтобы сделать вывод о возможности A. Как описывает A Beautiful Site, в тот момент, когда браузер выпускает одну возможность раньше другой, ваша проверка молча ломается. Не привязывайте проверки к косвенным признакам.

Второй тоньше: наличие не равно поддержке. Safari может сообщать через @supports значение true для значения, которое фактически не применяет, как обнаружили Evil Martians с ключевым словом выравнивания CSS safe. Для таких известных «лжецов» не доверяйте флагу поддержки. Отрендерите элемент за пределами экрана и измерьте реальный результат через 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;
};

Поведенческому тесту не нужны заявления о версиях, чтобы быть корректным: он наблюдает то, что реально отрисовалось. Это как раз тот класс багов, который хорошо выявляет session replay: возможность за гейтом проходит проверку поддержки в вашем тестовом окружении, но молча ведёт себя неправильно на реальной сборке iOS у пользователя.

Определение версии iOS через feature detection

Поскольку все браузеры на iOS работают на WebKit, а версия Safari привязана к мажорной версии ОС, проверка isMobileWebKit() вместе с гейтом CSS.supports() для свойства, появившегося в известном релизе Safari, позволяет вывести версию iOS вообще без разбора строки user-agent. Эвристика мобильного WebKit опирается на событие жеста, которое предоставляет WebKit:

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

Для гейта по версии найдите релиз, в котором свойство появилось впервые, в заметках о выпуске Safari от Apple или в данных о совместимости MDN, а затем проверьте его. Longhand-свойство text-wrap-style появилось в Safari 17.5, где значения balance, stable и auto вышли одновременно, поэтому любое из них чисто отсекает iOS 17.5+:

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

Проверьте соответствие «свойство ↔ релиз» самостоятельно перед выпуском. Заметки о выпуске иногда пропускают изменения, а флаг «поддерживается» может лгать (см. предыдущий раздел). Относитесь к isMobileWebKit() как к сильной эвристике, а не как к гарантии спецификации: Apple разрешает альтернативные браузерные движки в ЕС на iOS 17.4+, так что «iOS означает WebKit» — утверждение подавляюще верное, но не абсолютное.

Когда сниффинг user-agent — ограниченное последнее средство

Прибегайте к user-agent только тогда, когда одновременно верны две вещи. Для возможности действительно не существует feature-теста, и ошибочное предположение обойдётся вам не дороже косметического сбоя. Этой планке соответствуют два iOS-случая.

Старые iPad. Feature detection не может отличить iPad от Mac, потому что начиная с iPadOS 13 user-agent по умолчанию у iPad — та же строка, что отправляет Mac. Вместо этого комбинируйте сигналы. User-agent, который читается как десктопный Safari на macOS, в паре с положительной проверкой мобильного WebKit и ненулевым количеством точек касания означает iPad в одежде Mac. Проверка точек касания важна: Mac сообщает navigator.maxTouchPoints как 0, поэтому она не даст настоящему Mac совпасть, даже если сигнал события жеста присутствует в десктопном Safari.

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

Заморозка UA в iOS 26. В iOS 26 и iPadOS 26 Safari перестал указывать в своём user-agent версию работающей ОС и вместо этого зафиксировал токен на более раннем релизе. Само это зафиксированное значение менялось от одного точечного релиза к другому (18_6 на старте, затем 18_6_2 в Safari 26.1, затем 18_7, начиная с iOS 26.2) — именно поэтому его ни в коем случае нельзя хардкодить. Парсите токен Version/, который по-прежнему отражает реальную мажорную версию Safari (а значит, и iOS):

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

Два уточнения делают картину точнее. Во-первых, заморозка касается только Safari: независимый анализ серверных логов от AppleInsider и Lapcat Software подтверждают, что Chrome и Firefox на iOS по-прежнему сообщают реальную версию ОС, поэтому «нельзя определить iOS 26» — это проблема, специфичная для Safari, а не для iOS в целом. Во-вторых, если вы не хотите брать парсинг на себя, ua-parser-js в последнем релизе на npm имеет версию 2.0.10, так что фиксируйте 2.0.10+. У пакета также есть задокументированная история инцидентов с цепочкой поставок, поэтому проверяйте, что именно вы устанавливаете.

Порядок действий

Полный процесс короткий и выполняется по порядку:

  1. Сначала feature detection. 'x' in window, CSS.supports(), @supports, опциональная цепочка. Это покрывает подавляющее большинство решений о включении функциональности.
  2. Проверяйте «лжецов» поведенчески. Когда флагу поддержки нельзя доверять, рендерите за пределами экрана и измеряйте через getBoundingClientRect().
  3. Выводите версию iOS без UA. isMobileWebKit() плюс гейт CSS.supports() на свойстве, подтверждённом заметками о выпуске.
  4. Сниффьте только неопределяемые крайние случаи. Комбинация признаков для старых iPad и токен Version/ для заморозки в iOS 26. Используйте их только там, где неудачный сниффинг не приводит к потере функциональности.
  5. Тестируйте на реальных устройствах и симуляторах. И заметки о выпуске, и флаги поддержки оставляют пробелы, и только реальное железо ставит точку.

Feature detection — это подход по умолчанию, потому что он переживает обновления, которых вы не планировали; сниффинг user-agent — ограниченное исключение для тех двух-трёх iOS-случаев, которые платформа делает действительно неопределяемыми. Выстраивайте проверки именно в этом порядке, опирайтесь на парсинг токена Version/ вместо любой замороженной строки ОС и проверяйте результат на настоящем старом iPad перед релизом.

Часто задаваемые вопросы

Можно ли определить iOS 26 в JavaScript, несмотря на заморозку user-agent?

Да. Начиная с iOS 26 Safari замораживает токен ОС в своём user-agent на значении из версий до 26, которое менялось от релиза к релизу (18_6, затем 18_6_2, затем 18_7, начиная с iOS 26.2), поэтому хардкодить его нельзя. Вместо этого парсите токен Version/, который по-прежнему сообщает реальную мажорную версию Safari, а значит, и мажорную версию iOS. В Safari на iOS 26.x совпадение с Version/(\\d+) возвращает 26.

Влияет ли заморозка user-agent в iOS 26 на Chrome и Firefox на iOS?

Нет. Apple документирует замороженный токен ОС как собственное поведение Safari в iOS и iPadOS 26. Независимые анализы серверных логов от AppleInsider и Lapcat Software подтверждают, что Chrome и Firefox на iOS по-прежнему сообщают реальную версию ОС в своих строках user-agent, хотя все браузеры на iOS работают на WebKit. Так что «нельзя определить iOS 26» — это проблема, специфичная для Safari, а не для iOS в целом.

Как отличить iPad от Mac в браузере?

Комбинируйте два сигнала, поскольку одного feature detection недостаточно: начиная с iPadOS 13 user-agent по умолчанию у iPad — та же строка, что отправляет Mac. Если user-agent выглядит как десктопный Safari на macOS, но проверка мобильного WebKit указывает, что вы на устройстве с сенсорным экраном, перед вами iPad, маскирующийся под Mac. Поскольку события жестов присутствуют и в десктопном Safari, добавьте условие navigator.maxTouchPoints больше 0, так как Mac сообщает 0.

Работает ли navigator.userAgentData в Safari?

Нет. navigator.userAgentData доступен только в Chromium и помечен на MDN как экспериментальный, поэтому Safari и Firefox его не реализуют. Он никогда не является жизнеспособной заменой feature detection, если ваша цель — iOS Safari. Вместо этого проверяйте возможности напрямую через 'x' in window или CSS.supports() — MDN оценивает этот подход как значительно более надёжный, чем чтение 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.