- PVSM.RU - https://www.pvsm.ru -
Нашей команде поставили задачу разработать мобильное приложение для iOS и Android. Нативных разработчиков на Swift и Kotlin в команде нет, зато половина хорошо знает Vue. Поэтому я как лид выбрал Vue и Capacitor — фреймворк, который упаковывает обычное веб‑приложение в нативную оболочку и даёт доступ к функциям телефона через JavaScript‑плагины.
Через приложение клиенты заказывают охрану и транспорт с водителем. В нём есть:
конфигуратор и оформление заказа;
отслеживание исполнителя на карте;
вход по PIN‑коду или Face ID.
Стек: Vue 3, TypeScript, Vite и Capacitor 8.
По итогам тестирования приложение в целом ведёт себя как нативное. Для этого команде пришлось освоить то, с чем фронтендер обычно не работает:
нативные инструменты сборки на компьютерах с Windows, macOS и Linux;
вёрстку под вырез экрана и клавиатуру;
настройку нативных проектов;
раздачу тестовых сборок через TestFlight.
Vue 3, Vite, Tailwind, Vitest и Storybook работают так же, как в вебе. Что это даёт на практике:
Вёрстку и роутинг почти целиком проверяем в Chrome DevTools в режиме устройства.
Эмулятор нужен только для того, что зависит от платформы.
Горячая замена модулей (HMR) в Vite работает и на телефоне: достаточно указать адрес dev‑сервера в server.url.
Приложение на устройстве отлаживается как обычная страница: через Safari Web Inspector на iOS и chrome://inspect на Android.
Нативный разработчик на Swift или Kotlin команде не понадобился.
Для работы с возможностями смартфона у Capacitor есть большой набор плагинов: камера, геолокация, уведомления, файловая система, вибрация и другие. Официальные плагины поддерживает команда Capacitor, их список есть в документации [1]. Остальное, например биометрию, закрывают плагины сообщества из отдельного каталога [2].
Мы используем пять плагинов:
@capacitor/app — события жизненного цикла приложения и системные кнопки;
@capacitor/device — сведения об устройстве, например язык системы;
@capacitor/keyboard — управление экранной клавиатурой;
@aparajita/capacitor-biometric-auth — вход по Face ID и отпечатку пальца с имитацией биометрии в браузере;
capacitor-secure-storage-plugin — защищённое хранение данных в Keychain и Keystore.
Разберём подключение на примере @capacitor/keyboard.
Шаг 1. Установить пакет и обновить нативные проекты:
npm install @capacitor/keyboard
npx cap sync
Шаг 2. Настроить плагин в capacitor.config.ts. Параметр resize: None отключает сжатие WebView при открытии клавиатуры:
import type { CapacitorConfig } from "@capacitor/cli";
import { KeyboardResize } from "@capacitor/keyboard";
const config: CapacitorConfig = {
appId: "com.example.app",
appName: "Example",
webDir: "dist",
plugins: {
Keyboard: {
resize: KeyboardResize.None,
},
},
};
export default config;
Шаг 3. Вызвать API из кода приложения. Здесь панель над клавиатурой iOS скрывается, а высота клавиатуры передаётся в CSS‑переменную:
import { Capacitor } from "@capacitor/core";
import { Keyboard } from "@capacitor/keyboard";
if (Capacitor.getPlatform() === "ios") {
await Keyboard.setAccessoryBarVisible({ isVisible: false });
}
await Keyboard.addListener("keyboardWillShow", ({ keyboardHeight }) => {
document.documentElement.style.setProperty("--keyboard-height", `${keyboardHeight}px`);
});
Для каждой платформы Capacitor создаёт обычный нативный проект: Xcode для iOS и Android Studio для Android. Внутри него в WebView работает веб‑приложение, а собирается он стандартными инструментами платформы. В WebView доступно всё, что есть в браузере, поэтому мы использовали знакомые библиотеки и код, наработанный в других проектах.
По задумке Capacitor нативные проекты хранятся в репозитории [3] и правятся вручную. Можно пойти другим путём и пересоздавать их из конфигурации при каждой сборке. Мы выбрали этот вариант:
Плюс: проект всегда чистый, конфликтов при слиянии нет.
Минус: ручные правки теряются, поэтому нативные настройки приходится вносить скриптом.
Писать нативный код не нужно, но собирать нативные проекты всё равно придётся, а для этого нужны незнакомые фронтендеру инструменты: JDK, Android SDK, эмуляторы, Xcode. Все их нам пришлось изучить и настроить, а у каждого свои требования к версиям. Часть инструментов ставит менеджер версий, остальное — вручную:
Ставит менеджер версий mise:
JDK (Temurin 21), на котором работает Gradle;
Android SDK: Platform 36 и Build‑Tools 35.0.0;
образы эмулятора Android API 36 и API 34;
Xcode 26+ и раннер iOS‑симулятора, только на macOS.
Ставим вручную:
Android Studio: из неё нужны Device Manager и Logcat;
виртуальные устройства в Device Manager;
аппаратное ускорение эмулятора: в Windows — компонент Windows Hypervisor Platform, в Linux — KVM (Kernel‑based Virtual Machine).
Если ставить всё вручную, легко пропустить один из шагов, и тогда Gradle не найдёт SDK или не запустится эмулятор. К тому же никто не гарантирует, что у двух разработчиков окажутся одинаковые версии. Где легко ошибиться:
JAVA_HOME должен указывать на корень JDK, без /bin и без слэша в конце.
В SDK Manager нужно включить «Show Package Details», иначе установятся не те версии. Флажок «Command‑line Tools» по умолчанию снят, а без него не будет sdkmanager.
Gradle JDK в настройках Android Studio задаётся отдельно: там тоже нужен Temurin 21.
Путь в ANDROID_HOME в Windows, macOS и Linux разный. В PATH добавляются три каталога, после чего придётся открыть новый терминал и полностью перезапустить VS Code.
Чтобы не повторять эту настройку на каждой машине, мы освоили менеджер версий mise. Привычные фронтендеру nvm и Volta фиксируют только версию Node, а SDKMAN — только JVM‑инструменты. У mise возможностей больше:
закрепляет версии Node, JDK, Android cmdline‑tools и xcodes;
задаёт переменные окружения;
описывает задачи установки;
работает одинаково на Windows, macOS и Linux.
Всё это хранится в одном файле mise.toml. Упрощённый пример:
# Версии инструментов: mise скачает их и включит при входе в папку проекта
[tools]
java = "temurin-21" # JDK для Gradle, который собирает Android-проект
node = "24"
android-sdk = "23.0" # утилиты командной строки Android SDK
xcodes = { version = "latest", os = ["macos"] } # установщик Xcode, только на macOS
# Переменные окружения, которые mise выставляет в папке проекта
[env]
# SDK лежит в стандартном для ОС каталоге, а не в каталоге mise:
# так он общий с Android Studio и образы не скачиваются дважды
ANDROID_HOME = "{% if os() == 'windows' %}{{ env.LOCALAPPDATA }}\Android\Sdk{% elif os() == 'macos' %}{{ env.HOME }}/Library/Android/sdk{% else %}{{ env.HOME }}/Android/Sdk{% endif %}"
# adb и emulator становятся доступны в терминале
_.path = ["{{ env.ANDROID_HOME }}/platform-tools", "{{ env.ANDROID_HOME }}/emulator"]
# Переменные для задач
[vars]
# ABI образа эмулятора: arm64 на Apple silicon, x86_64 на Intel и AMD
abi = "{% if arch() == 'arm64' or arch() == 'aarch64' %}arm64-v8a{% else %}x86_64{% endif %}"
# Задачи запускаются командой mise run <имя задачи>
[tasks."android:sdk"]
description = "Пакеты Android SDK для сборки"
run = "android sdk install platform-tools platforms/android-36 build-tools/35.0.0"
[tasks."android:emulator"]
description = "Эмулятор и образы Android API 36 и API 34"
depends = ["android:sdk"] # сначала выполнится android:sdk
run = "android sdk install emulator system-images/android-36/google_apis/{{ vars.abi }} system-images/android-34/google_apis/{{ vars.abi }}"
[tasks."ios:simulator"]
description = "Раннер iOS-симулятора, только macOS"
run = "xcodebuild -downloadPlatform iOS"
Новому разработчику достаточно запустить mise install и задачу установки. Если чего‑то не хватает, задача проверки окружения покажет, чего именно, и разбираться в ошибках xcodebuild придётся реже. Подробнее о секциях и задачах написано в документации mise [4].
В автоматизации Android SDK через mise нашлись три подвоха:
cmd в Windows разбивает строку по ; даже внутри кавычек, поэтому пакет "platforms;android-36" превращался в два несуществующих. Пришлось перейти с sdkmanager на новый Android CLI, где имена пакетов пишутся через слэш.
Android CLI возвращает код 9 даже при успешной установке. Этот код приходится игнорировать, а установку проверять по наличию каталогов пакетов.
avdmanager не видит образы, установленные новым CLI, а android emulator create не позволяет задать имя и уровень API. В итоге виртуальные устройства по‑прежнему приходится создавать вручную в Device Manager.
Больше всего правок потребовали вещи, которых нет в десктопном браузере: вырез и полоска «домой», экранная клавиатура, сенсорные жесты и особенности WebView. Примерно каждый четвёртый баг на тестировании касался отрисовки на телефоне, причём половина таких багов проявлялась только на iPhone.

Два бага связаны с безопасными зонами экрана. На iPhone шапка заезжала под часы и индикаторы: без viewport-fit=cover переменные env(safe-area-inset-*) не работают. Теперь viewport-fit=cover добавляется при запуске на iOS, а шапке задан отступ env(safe-area-inset-top). Низ шторки уходил под полоску «домой» из‑за фиксированного нижнего отступа, его заменил отступ calc(5.125rem + env(safe-area-inset-bottom)).
Три бага связаны с касаниями. Двойной тап на iPhone увеличивал масштаб, как на сайте, потому что масштабирование не было запрещено в viewport: помогли user-scalable=no и touch-manipulation на body. На iPhone и Android после касания оставалась синяя рамка фокуса, так как браузерный outline не был отключён для сенсорного ввода. Класс pointer-coarse:outline-none убрал её для касаний и сохранил для мыши и клавиатуры. На экране шириной 360 px не прокручивался ряд иконок: touch-none на ручке шторки перехватывал все касания. С touch-pan-x ряд прокручивается, а шторка двигается только вертикальным жестом.
Ещё три бага касаются прокрутки и анимаций. Шторку на iPhone не удавалось докрутить до конца, потому что сдвиг параллакса не учитывался в высоте прокрутки, и его компенсирует невидимый отступ на ту же величину. На вытянутых экранах вокруг интро‑анимации появлялись белые полосы: Lottie вписывает кадр 1179×2556 целиком, а параметр preserveAspectRatio: "xMidYMid slice" заполняет им экран. Стекло шапки на iPhone и Android теряло размытие во время перехода между экранами, так как backdrop-filter в отдельном слое View Transitions рисуется прямоугольником. Теперь в отдельный слой выносится только ряд кнопок, а стекло остаётся в снимке страницы.
Остаются баги с клавиатурой. На iPhone над ней появлялась лишняя панель, а саму клавиатуру нельзя было закрыть: так WebView ведёт себя по умолчанию. Это исправили плагин @capacitor/keyboard, атрибут enterkeyhint="done" и закрытие клавиатуры при прокрутке. На Android фон «сжимается» при открытии клавиатуры, потому что градиент растянут по высоте экрана, а видимая высота WebView уменьшается. Этот баг пока не исправлен.
Режим устройства в Chrome DevTools не показывает реальную версию WebView, системные отступы (вырез, строка состояния), масштаб системного шрифта и нативные диалоги вроде Face ID. Приложение работает внутри системного WebView, а его версия и поведение зависят от устройства. На iOS движок привязан к версии системы, поэтому на старых iPhone часть веб‑API недоступна.
На Android WebView обновляется отдельно, через Play Store, но поведение самой системы меняется от версии к версии: начиная с Android 15 приложение принудительно отображается под системными панелями (edge‑to‑edge). Поэтому в проекте два образа эмулятора: API 36 (целевая версия) и API 34 (поведение до edge‑to‑edge). На практике сложную вёрстку лучше проверять на реальном iPhone или в симуляторе и отлаживать через Safari → Develop или chrome://inspect.
Нативные проекты, которые создаёт Capacitor, и готовые плагины закрывают большую часть задач. Для остального понадобилось несколько нативных настроек и пара обработчиков поведения платформы.
Если нативные проекты пересоздаются, ручные правки в Xcode и Android Studio теряются. Поэтому все нативные настройки мы вносим отдельным скриптом: он запускается при каждой сборке, а повторный запуск ничего не ломает. Что дописывает скрипт и что будет без этого:
NSFaceIDUsageDescription в Info.plist. Без него вход по Face ID падает при первом же запросе.
UIApplicationSceneManifest и SceneDelegate. Без них приложение, собранное в Xcode 27, закрывается сразу после запуска.
ITSAppUsesNonExemptEncryption = false. Без этого App Store Connect для каждой сборки задаёт вопрос о шифровании, и, пока ответа нет, сборка недоступна тестировщикам.
У скрипта есть слабое место: его нужно запускать везде, где собирается приложение, включая CI. Иначе сборка уйдёт без нужных настроек, например без разрешения на Face ID.
Кнопка «Назад» на Android. По умолчанию она закрывала приложение, а не возвращала на предыдущий экран. Понадобился обработчик, связанный с vue‑router. Мы пока не проверяли, закрывает ли он открытую шторку, а не уводит со страницы.
Видео после сворачивания. iOS ставит его на паузу, поэтому слайдер теперь отслеживает видимость страницы через useDocumentVisibility.
Аналога APK на iOS нет, поэтому просто отправить файл тестировщику нельзя. С бесплатным Apple ID сборка работает семь дней и только на телефоне разработчика, а для TestFlight нужно участие в Apple Developer Program за $99 в год. Собрать iOS‑приложение можно только на macOS, поэтому раздачу удобно вынести в облачный CI с Mac, например Codemagic: он подписывает сборку и сам загружает её в TestFlight.
Помимо самого приложения нужно поддерживать набор скриптов для удобной работы с ним. Эти скрипты:
запускают dev‑сервер и приложение на эмуляторе или симуляторе с live‑reload: находят уже запущенное устройство или поднимают новое и настраивают adb reverse;
проверяют окружение и показывают, чего не хватает на машине;
создают нативные проекты для Android и iOS;
вносят нативные настройки в сгенерированные проекты;
генерируют иконки и заставку приложения из исходных изображений;
собирают iOS‑приложение и загружают его в TestFlight.
Собрать iOS‑приложение можно только в Xcode, а Xcode работает только на macOS. Поэтому хотя бы у одного разработчика в команде должны быть Mac и iPhone: симулятор не заменяет проверку на реальном устройстве. Подойдёт не любой Mac:
Capacitor 8 требует Xcode 26 или новее (документация Capacitor [5]).
Xcode 26–26.3 запускается на macOS Sequoia 15.6 и новее.
Xcode 26.4 и новее требует macOS Tahoe 26.2, а Xcode 27 — Tahoe 26.6 (системные требования Xcode [6]).
macOS Tahoe ставится на любой Mac с Apple silicon, а из компьютеров на Intel — только на несколько моделей 2019–2020 годов. На остальных Intel‑Mac потолок — Xcode 26.3.
Ограничения задаёт Apple, а не Capacitor: с 28 апреля 2026 года App Store Connect принимает только сборки из Xcode 26 и новее [7], поэтому Flutter и React Native сталкиваются с теми же ограничениями. На практике для iOS‑разработки стоит брать Mac на Apple silicon.
По умолчанию открытая клавиатура уменьшает высоту WebView, и вместе с ним перерисовываются фон, градиент и шапка. На iOS мы отключили это поведение (Keyboard.resize: None в capacitor.config.ts), но тогда поле в фокусе перестало само подниматься над клавиатурой. Пришлось написать собственный модуль подъёма полей. Он:
находит контейнер, внутри которого прокручивается поле;
плавно сдвигает содержимое с учётом параллакса;
добавляет временный отступ, если места не хватает;
выставляет CSS‑переменную с высотой клавиатуры, по которой поднимаются шторки и экран входа.
Почти сразу понадобилась поправка: анимация подъёма конфликтовала с касаниями, и теперь она останавливается, как только пользователь дотрагивается до экрана. В нативном приложении эту работу делает система. В Capacitor есть два варианта: либо WebView сжимается и фон перерисовывается вместе с ним, либо подъём полей нужно написать самим.
Приложение, собранное в Xcode 27, закрывается сразу после запуска: в шаблоне Capacitor 8.4 нет сценового жизненного цикла, а стабильного релиза с исправлением пока нет. Ключ UIApplicationSceneManifest в Info.plist и класс SceneDelegate пришлось добавлять в нативный проект самим.
Движок WebView на iPhone обновляется только вместе с системой, поэтому новые веб‑API получают лишь пользователи свежих версий iOS. View Transitions, например, работают начиная с iOS 18, и на более старых телефонах страницы сменяются без анимации.
Capacitor себя оправдал: веб‑команда без нативных разработчиков собрала приложение для двух платформ из одной кодовой базы, и почти все проблемы решались веб‑средствами или короткими скриптами. Принцип «написал один раз — работает везде» в целом сработал, но без доработок не обошлось: примерно каждый четвёртый баг на тестировании касался экрана телефона, а сборка держится на наборе скриптов и настроенном mise.
Подходит, если интерфейс состоит из форм, карточек, списков и карт, команда сильна в вебе, а функции устройства нужны стандартные и на них есть готовые плагины.
Стоит заложить больше времени, если продукт держится на сложных жестах, шторках и анимациях, которые должны ощущаться как нативные: их пришлось дорабатывать дольше всего. То же относится к нативным компонентам поверх веба. А для iOS‑версии понадобятся Mac и аккаунт разработчика Apple.
Закрепить версии Node, JDK и Android SDK в mise.toml и использовать те же версии в CI.
Учесть безопасные зоны ещё до начала вёрстки: viewport-fit=cover, env(safe-area-inset-*) в шапке и нижних панелях, запрет масштабирования.
Выбрать стратегию работы с клавиатурой до первой формы: режим resize, панель над клавиатурой, enterkeyhint.
Хранить нативные настройки в скрипте и запускать его и локально, и в CI.
Сразу обработать кнопку «Назад» на Android и возврат приложения из фона.
Проверять вёрстку на реальном iPhone и на двух уровнях Android API, а не только в DevTools.
Оборачивать плагины сообщества в собственный драйвер с тестами, как мы сделали с защищённым хранилищем.
Приложение пока на тестировании, в App Store и Google Play оно ещё не вышло. После релиза расскажем, как оно показало себя у реальных пользователей.
Capacitor
Документация Capacitor [8] — с чего начать.
Настройка окружения [5] — что поставить для сборки под iOS и Android.
Официальные плагины [1] и каталог плагинов сообщества [2].
Capacitor Community [9] и Capawesome [10] — ещё два больших набора плагинов.
Live reload на устройстве [11] — как подключить приложение к dev‑серверу.
Создание своего плагина [12] — если готового нет.
Иконки и заставка [13] — генерация из исходных изображений.
Окружение
mise [4]: конфигурация [14] и задачи [15].
xcodes [16] — установка и переключение версий Xcode из терминала.
Системные требования Xcode [6] — какая macOS нужна для каждой версии.
Требования App Store к SDK [7] — с каким Xcode принимают сборки.
Вёрстка и отладка на телефоне
env() на MDN [17] — безопасные зоны через env(safe-area-inset-*).
Метатег viewport на MDN [18] — viewport-fit=cover и запрет масштабирования.
View Transitions на Can I use [19] — с каких версий браузеров работает.
Отладка WebView в Chrome [20] — chrome://inspect для Android.
Web Inspector в Safari [21] — отладка на iPhone и в симуляторе.
Сборка и раздача
TestFlight [22] — раздача iOS‑сборок тестировщикам.
Codemagic для Capacitor [23] — облачная сборка на Mac и загрузка в TestFlight.
Автор: sowa_dev
Источник [24]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/460072
Ссылки в тексте:
[1] документации: https://capacitorjs.com/docs/apis
[2] отдельного каталога: https://capacitorjs.com/docs/plugins/community
[3] хранятся в репозитории: https://capacitorjs.com/docs/cordova
[4] документации mise: https://mise.jdx.dev/
[5] документация Capacitor: https://capacitorjs.com/docs/getting-started/environment-setup
[6] системные требования Xcode: https://developer.apple.com/xcode/system-requirements
[7] сборки из Xcode 26 и новее: https://developer.apple.com/news/upcoming-requirements/
[8] Документация Capacitor: https://capacitorjs.com/docs
[9] Capacitor Community: https://github.com/capacitor-community
[10] Capawesome: https://capawesome.io/plugins/
[11] Live reload на устройстве: https://capacitorjs.com/docs/guides/live-reload
[12] Создание своего плагина: https://capacitorjs.com/docs/plugins/creating-plugins
[13] Иконки и заставка: https://capacitorjs.com/docs/guides/splash-screens-and-icons
[14] конфигурация: https://mise.jdx.dev/configuration.html
[15] задачи: https://mise.jdx.dev/tasks/
[16] xcodes: https://github.com/XcodesOrg/xcodes
[17] env() на MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/env
[18] Метатег viewport на MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/name/viewport
[19] View Transitions на Can I use: https://caniuse.com/view-transitions
[20] Отладка WebView в Chrome: https://developer.chrome.com/docs/devtools/remote-debugging/webviews
[21] Web Inspector в Safari: https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios
[22] TestFlight: https://developer.apple.com/testflight/
[23] Codemagic для Capacitor: https://docs.codemagic.io/yaml-quick-start/building-an-ionic-app/
[24] Источник: https://habr.com/ru/articles/1094782/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1094782
Нажмите здесь для печати.