Нашей команде поставили задачу разработать мобильное приложение для iOS и Android. Нативных разработчиков на Swift и Kotlin в команде нет, зато половина хорошо знает Vue. Поэтому я как лид выбрал Vue и Capacitor — фреймворк, который упаковывает обычное веб‑приложение в нативную оболочку и даёт доступ к функциям телефона через JavaScript‑плагины.
Через приложение клиенты заказывают охрану и транспорт с водителем. В нём есть:
-
конфигуратор и оформление заказа;
-
отслеживание исполнителя на карте;
-
вход по PIN‑коду или Face ID.
Стек: Vue 3, TypeScript, Vite и Capacitor 8.
По итогам тестирования приложение в целом ведёт себя как нативное. Для этого команде пришлось освоить то, с чем фронтендер обычно не работает:
-
нативные инструменты сборки на компьютерах с Windows, macOS и Linux;
-
вёрстку под вырез экрана и клавиатуру;
-
настройку нативных проектов;
-
раздачу тестовых сборок через TestFlight.
Что мы получили от Capacitor
Один стек на всё
Vue 3, Vite, Tailwind, Vitest и Storybook работают так же, как в вебе. Что это даёт на практике:
-
Вёрстку и роутинг почти целиком проверяем в Chrome DevTools в режиме устройства.
-
Эмулятор нужен только для того, что зависит от платформы.
-
Горячая замена модулей (HMR) в Vite работает и на телефоне: достаточно указать адрес dev‑сервера в
server.url. -
Приложение на устройстве отлаживается как обычная страница: через Safari Web Inspector на iOS и
chrome://inspectна Android. -
Нативный разработчик на Swift или Kotlin команде не понадобился.
Нативные функции через легко подключаемые плагины
Для работы с возможностями смартфона у Capacitor есть большой набор плагинов: камера, геолокация, уведомления, файловая система, вибрация и другие. Официальные плагины поддерживает команда Capacitor, их список есть в документации. Остальное, например биометрию, закрывают плагины сообщества из отдельного каталога.
Мы используем пять плагинов:
-
@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 нативные проекты хранятся в репозитории и правятся вручную. Можно пойти другим путём и пересоздавать их из конфигурации при каждой сборке. Мы выбрали этот вариант:
-
Плюс: проект всегда чистый, конфликтов при слиянии нет.
-
Минус: ручные правки теряются, поэтому нативные настройки приходится вносить скриптом.
Сложность 1: придётся освоить нативный тулчейн
Писать нативный код не нужно, но собирать нативные проекты всё равно придётся, а для этого нужны незнакомые фронтендеру инструменты: 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: ещё один инструмент, который пришлось освоить
Чтобы не повторять эту настройку на каждой машине, мы освоили менеджер версий mise. Привычные фронтендеру nvm и Volta фиксируют только версию Node, а SDKMAN — только JVM‑инструменты. У mise возможностей больше:
-
закрепляет версии Node, JDK, Android cmdline‑tools и
xcodes; -
задаёт переменные окружения;
-
описывает задачи установки;
-
работает одинаково на Windows, macOS и Linux.
Всё это хранится в одном файле mise.toml. Упрощённый пример:
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.
Грабли, на которые наступили по дороге
В автоматизации Android SDK через mise нашлись три подвоха:
-
cmdв Windows разбивает строку по;даже внутри кавычек, поэтому пакет"platforms;android-36"превращался в два несуществующих. Пришлось перейти сsdkmanagerна новый Android CLI, где имена пакетов пишутся через слэш. -
Android CLI возвращает код 9 даже при успешной установке. Этот код приходится игнорировать, а установку проверять по наличию каталогов пакетов.
-
avdmanagerне видит образы, установленные новым CLI, аandroid emulator createне позволяет задать имя и уровень API. В итоге виртуальные устройства по‑прежнему приходится создавать вручную в Device Manager.
Сложность 2: экран телефона отличается от окна браузера
Больше всего правок потребовали вещи, которых нет в десктопном браузере: вырез и полоска «домой», экранная клавиатура, сенсорные жесты и особенности 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.
Сложность 3: нативную часть иногда нужно донастроить
Нативные проекты, которые создаёт Capacitor, и готовые плагины закрывают большую часть задач. Для остального понадобилось несколько нативных настроек и пара обработчиков поведения платформы.
Патчи вместо правок в Xcode
Если нативные проекты пересоздаются, ручные правки в 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.
Раздача iOS‑сборок
Аналога 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 нужны Mac и iPhone
Собрать iOS‑приложение можно только в Xcode, а Xcode работает только на macOS. Поэтому хотя бы у одного разработчика в команде должны быть Mac и iPhone: симулятор не заменяет проверку на реальном устройстве. Подойдёт не любой Mac:
-
Capacitor 8 требует Xcode 26 или новее (документация Capacitor).
-
Xcode 26–26.3 запускается на macOS Sequoia 15.6 и новее.
-
Xcode 26.4 и новее требует macOS Tahoe 26.2, а Xcode 27 — Tahoe 26.6 (системные требования Xcode).
-
macOS Tahoe ставится на любой Mac с Apple silicon, а из компьютеров на Intel — только на несколько моделей 2019–2020 годов. На остальных Intel‑Mac потолок — Xcode 26.3.
Ограничения задаёт Apple, а не Capacitor: с 28 апреля 2026 года App Store Connect принимает только сборки из Xcode 26 и новее, поэтому Flutter и React Native сталкиваются с теми же ограничениями. На практике для iOS‑разработки стоит брать Mac на Apple silicon.
Клавиатура обошлась дороже всего
По умолчанию открытая клавиатура уменьшает высоту WebView, и вместе с ним перерисовываются фон, градиент и шапка. На iOS мы отключили это поведение (Keyboard.resize: None в capacitor.config.ts), но тогда поле в фокусе перестало само подниматься над клавиатурой. Пришлось написать собственный модуль подъёма полей. Он:
-
находит контейнер, внутри которого прокручивается поле;
-
плавно сдвигает содержимое с учётом параллакса;
-
добавляет временный отступ, если места не хватает;
-
выставляет CSS‑переменную с высотой клавиатуры, по которой поднимаются шторки и экран входа.
Почти сразу понадобилась поправка: анимация подъёма конфликтовала с касаниями, и теперь она останавливается, как только пользователь дотрагивается до экрана. В нативном приложении эту работу делает система. В Capacitor есть два варианта: либо WebView сжимается и фон перерисовывается вместе с ним, либо подъём полей нужно написать самим.
В Capacitor пока нет поддержки Xcode 27
Приложение, собранное в Xcode 27, закрывается сразу после запуска: в шаблоне Capacitor 8.4 нет сценового жизненного цикла, а стабильного релиза с исправлением пока нет. Ключ UIApplicationSceneManifest в Info.plist и класс SceneDelegate пришлось добавлять в нативный проект самим.
На iPhone возможности WebView зависят от версии iOS
Движок WebView на iPhone обновляется только вместе с системой, поэтому новые веб‑API получают лишь пользователи свежих версий iOS. View Transitions, например, работают начиная с iOS 18, и на более старых телефонах страницы сменяются без анимации.
Итог: когда Capacitor стоит брать
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 — с чего начать.
-
Настройка окружения — что поставить для сборки под iOS и Android.
-
Capacitor Community и Capawesome — ещё два больших набора плагинов.
-
Live reload на устройстве — как подключить приложение к dev‑серверу.
-
Создание своего плагина — если готового нет.
-
Иконки и заставка — генерация из исходных изображений.
Окружение
-
mise: конфигурация и задачи.
-
xcodes — установка и переключение версий Xcode из терминала.
-
Системные требования Xcode — какая macOS нужна для каждой версии.
-
Требования App Store к SDK — с каким Xcode принимают сборки.
Вёрстка и отладка на телефоне
-
env() на MDN — безопасные зоны через
env(safe-area-inset-*). -
Метатег viewport на MDN —
viewport-fit=coverи запрет масштабирования. -
View Transitions на Can I use — с каких версий браузеров работает.
-
Отладка WebView в Chrome —
chrome://inspectдля Android. -
Web Inspector в Safari — отладка на iPhone и в симуляторе.
Сборка и раздача
-
TestFlight — раздача iOS‑сборок тестировщикам.
-
Codemagic для Capacitor — облачная сборка на Mac и загрузка в TestFlight.
Автор: sowa_dev
