Разбор · 24.09.2026 · до решений владельца код не пишется

По душе на iOS

Что нужно, чтобы собрать «По душе» для iPhone так, чтобы приложение совпадало с Android по функциям и вёрстке экранов: инвентарь кода, сравнение путей, проверенная сегодня сборка на макмини, требования Apple и России, план по этапам и вопросы владельцу.

549 / 623кадров Figma зелёные в Android-приложении — эта вёрстка и должна попасть на iOS
22 из 190файлов Kotlin привязаны к Android; остальное собирается под iOS как есть
1170 × 2532экран симулятора iPhone 16e/17e — ровно кадр Figma ×3, тот же, что у эталона Android
8–12 недельдо сверенной сборки в TestFlight, если владелец закрывает свои пункты вовремя (оценка)

0Коротко

  1. iOS-кода «По душе» нет ни в репозитории, ни на макмини, ни в трекере. В папке ios/ лежит SwiftUI-клиент другого продукта, «По любви». Спека фронта DUSHA-A-17 обещала «затем iOS», папки ios-events нет. «iOS: 42 из 70 экранов» на созвоне 17.09 — это перевёрстка «По любви» (LYUBVI-39).
  2. Требование «та же вёрстка, те же функции» выполняет только Compose Multiplatform. Те же 190 файлов (46 204 строки, 54 маршрута, 623 кадра) собираются под iOS тем же движком раскладки. SwiftUI, Flutter, React Native, веб-обёртка — второй интерфейс с нуля и повторная сверка 623 кадров на другом текстовом движке. Skip переводит только Swift → Kotlin, а не наш Compose → SwiftUI.
  3. Переезжает как есть 85–90 % кода. Платформенное — 22 файла: двигатель теней и размытий на android.graphics, нативный канвас в 8 экранах, карта MapLibre, камера селфи, выбор фото, разрешения, геолокация, окно макета. Плюс замена шести библиотек и перенос картинок, строк и шрифтов в ресурсы Compose.
  4. Честная граница паритета — текст. Блоки совпадут один в один. Текст на iOS считает и растрирует другой движок (Skia + CoreText против android.text + FreeType) — расхождения до 1 px по строке. Наша метрика сверки уже меряет геометрию «без текста» и текст по плотности чернил, конвейер переносится без изменений. Эмодзи будут яблочные, отскок прокрутки — как в iOS.
  5. Макмини готов, и это проверено сегодня. Xcode 26.6 (Apple принимает сборки только на Xcode 26), сертификаты команды Doktor Svat, ключ App Store Connect, рабочий конвейер архив → IPA → TestFlight от «По любви». Пробный проект Compose Multiplatform собрался, установился и запустился в симуляторе iPhone 17e: размытие, стекло, режимы наложения на Skia, кириллица, навигация — раздел 5.
  6. Apple и Россия. Для входа по телефону Sign in with Apple не нужен. Обязательны удаление аккаунта в приложении, жалобы и блокировка, демо-номер с фиксированным кодом для ревьюера, политика по ссылке, возрастная анкета. TestFlight в России ставится, App Store с июля 2026 открывается с перебоями. Продление членства $99 — только зарубежной картой. Главный юридический риск — селфи-верификация: по 572-ФЗ это биометрия, нужен юрист.
  7. Пушей в Android «По душе» нет — в первой iOS-версии их тоже нет. Когда появятся, на iOS их надо слать с сервера напрямую в APNs, без Firebase.
  8. Оценка: 8–12 недель до сверенной сборки в TestFlight одним исполнителем-агентом. Это оценка, не обещание: главные неизвестные — карта на iOS и разбор текстовых расхождений по 623 кадрам.

1Что есть и чего нет

Android «По душе» (android-events/)

ЧтоКак есть
Код190 файлов Kotlin, 46 204 строки: экраны 25 004, кит и компоненты 10 991, данные 4 747
СтекKotlin 2.0.21, Compose 1.7.6 (BOM 2024.12.01), AGP 8.7.3, Gradle 8.13, minSdk 31
Навигацияnavigation-compose 2.8.5, 54 маршрута, deep links podushe://event, podushe://user
СетьRetrofit + OkHttp, 46 ручек в двух API, 5 загрузок файлов, WebSocket чата, обновление токенов с мьютексом
ХранилищеDataStore Preferences 1.1.1 — уже кроссплатформенный
ГрафикаFigDraw.kt: тени и размытия Figma треугольным ядром на битмапе ALPHA_8, Paint, DST_OUT, кэш LruCache. Стекло — через GraphicsLayer + BlurEffect (общий API). Нативный канвас ещё в 8 экранах
КартаMapLibre Android 11.5.2 + OpenFreeMap; маркеры и кластеры — растровые символы, нарисованные StaticLayout; снимки мини-карт MapSnapshotter; лист места на SurfaceView
Камера, фото, геоCameraX (селфи), системный выбор фото, LocationManager без Google-сервисов, Photon + системный геокодер
ЖурналSentry Android 8.57 + инструментация Log.* (GlitchTip, DSN пока пустой)
ПрочееZXing (QR), Coil 2.7, java.time в 29 файлах, material-icons-extended подключена, но не используется ни разу
Ресурсы477 картинок (108 векторных XML из .fig, 333 PNG, 25 JPG; 104 МБ картинок макета), 6 шрифтов SF Pro, 1 542 строки
Тесты18 файлов; Paparazzi в геометрии 1170×2532 — то есть iPhone; 62 эталона; debug-экран съёмки кадров и 33 файла кадров
Паритет с Figma623 кадра, 549 зелёных по геометрии; 74 красных — растр карты (30), размытие листа регистрации (13), перевыборка фото и стекло (31)
Пушинет: ни Firebase, ни регистрации токена. Сервер умеет только FCM
Темп правок11 коммитов за последние 7 дней, параллельные сессии правят экраны ежедневно

Макмини (agent@100.85.254.75), проверено 24.09

ЧтоКак есть
Железо, ОСApple M4 Pro, 14 ядер, 64 ГБ, свободно 566 ГБ; macOS 26.5
Xcode26.6, SDK iOS 26.5, лицензия принята; симуляторы iOS 26.3–26.5: iPhone 16e, 17e, 17, 17 Pro, Air, 16, 14
Экран симулятораiPhone 16e и 17e: 1170×2532, масштаб 3 — тот же кадр, что рендеры Figma и эталонный эмулятор Android
Java, GradleJDK 17, кэш Gradle 6,6 ГБ; Kotlin/Native до сегодняшнего дня не ставился, появился в пробной сборке (1,6 ГБ)
Сетьпрямой выход в интернет и DNS работают; DNS-шим больше не обязателен
ПодписьApple Distribution ×2 «Doktor Svat, Ooo» (N63B7D662A), Apple Development (Вадим), 2 профиля, ключ App Store Connect API
Конвейерскрипты архив → IPA → altool; «По любви» dushalove 1.0.0 (1) в TestFlight с 01.09
НетHomebrew, CocoaPods, fastlane; xcodegen есть в ~/tools

Трекер и документы

2Пути к iOS и почему подходит один

Вёрстка Android выверена по нодам Figma и продолжает выверяться. Любой путь, где экраны iOS пишутся заново, — это второй проход всей программы паритета на другом движке.

ПутьСовпадение вёрстки с AndroidПовторное использование 46 тыс. строкКто пишетСрок
A. Compose MultiplatformРаскладка идентична (один код, один движок); текст ±1 px; эмодзи и отскок прокрутки платформенные≈85–90 % как есть, 22 файла в двух вариантах, 6 библиотек заменитьKotlin + агенты, как сейчас8–12 недель
B. SkipНет: Skip переводит Swift → Kotlin или компилирует Swift под Android; UI на iOS — SwiftUI. Compose он не переносит0 %Swiftмесяцы, обе платформы пересверять
C. SwiftUI/UIKit с нуляНет по конструкции: Core Text, системная прокрутка, точная высота строки только с iOS 260 %Swift, без iOS-разработчика8+ месяцев; опыт «По любви»: 42 из 70 экранов за недели, «оценки ненадёжны»
D. FlutterМежду iOS-Flutter и Android-Flutter — да; с нашим Android-Compose — нет0 % (Dart)Dartпереписывание обеих платформ
E. React Native / KMP + SwiftUIНет: текст меряет платформа / UI на SwiftUIRN 0 %; KMP — только данныеTS / Swiftкак C
F. PWA / CapacitorНет (браузерный движок); риск отказа Apple по 4.20 %Webкак C
Вывод: A. У общего кода есть ещё одно свойство, важное для проекта: каждая правка DUSHA-xx после переезда делается один раз и попадает в обе платформы. На путях B–F каждую правку экрана делают и сверяют дважды.

JetBrains объявила iOS стабильным в Compose Multiplatform 1.8 (май 2025); текущая стабильная — 1.12.1 (22.09.2026). В проде на нём целиком или почти целиком: Sony, Bitkey (95 % общего кода), Markaz (100+ экранов, порт на iOS за 28 дней), Instabee, Wrike, Feres, Respawn (96 %), Balary (карта MapLibre и видео — нативными вью).

3Что такое перенос нашего кода

Структура

Сейчас один Android-модуль. Станет: общий модуль с commonMain (весь UI, состояния, репозитории), androidMain (то, что и сейчас платформенное), iosMain (реализации для iOS) и папка iosApp — Xcode-проект из десятка строк Swift, генерируется xcodegen как у «По любви». Android-сборка остаётся прежней: те же флейворы, тот же APK для тестировщиков.

Обязательное условие — обновление Kotlin 2.0.21 → 2.4.20 и Compose 1.7.6 → 1.12. Это правка «двигателя»: по правилу проекта после неё пересъёмка и сравнение всех 623 кадров на Android, до любого переезда. Куда смотреть первыми по релиз-нотам Compose 1.8–1.12: убран неявный слой у BasicText (тексты с режимами наложения и альфой), незагрузившийся шрифт молча заменяется системным, оптимизация текста при lineHeight + softWrap = false (ровно наш PdNodeText), пересчёт размера полей ввода.

Переезжает механически

ЧтоСейчасСтанетОбъём
Картинки, векторы, шрифтыres/drawable*, res/font, painterResource(R.drawable.x) в 47 файлахресурсы Compose, Res.drawable.x; векторные XML Android поддерживаются как есть177 ссылок
Строкиstrings.xml, stringResource в 68 файлах, числительныеRes.string.x, Res.plurals.x650 ссылок
Адреса стендовBuildConfig по флейворуBuildKonfig или константы по таргету; на iOS — конфигурации Xcode Dusha/Dushalove/Prod с разными bundle id5 флейворов
Датыjava.time в 29 файлах (русские месяцы, форматы)kotlinx-datetime 0.8 со своими таблицами названий~60 мест
Локаль, ресурсыLocale("ru") в 13 файлахлокаль и ресурсы Compose13 файлов
Навигация, ViewModelandroidx 2.8.xте же API от JetBrains: navigation 2.9.2, lifecycle 2.11.0импорт

Пишется в двух вариантах (22 файла)

УзелAndroid (есть)iOS (написать)Оценка
Двигатель теней и размытий FigDraw.ktBitmap ALPHA_8, Paint с BlurMaskFilter и DST_OUT, LruCacheто же на Skia: org.jetbrains.skia.Bitmap, MaskFilter.makeBlur (принимает σ = R/2 напрямую), BlendMode.DST_OUT; ядро размытия — общий код3–5 дней
Нативный канвас в 8 экранахandroid.graphics.Canvas, Paint с PLUS и градиентамиskiaCanvas с теми же 29 режимами наложения; LINEAR_BURN и там и там считается формулой — общий код; вынести в фасад2–3 дня
СтеклоGraphicsLayer + BlurEffect — общий APIкак есть; в пробе на iOS работает (запись слоя — с явной плотностью)проверка
Карта EventMap.ktMapLibre Android, символьные слои, маркеры-битмапы, MapSnapshotterрекомендация: UIKitView + MLNMapView из MapLibre Native iOS 6.31 (11.09.2026), маркеры рисовать Skia, снимки MLNMapSnapshotter. Библиотека maplibre-compose 0.17 на iOS — beta поверх нестабильного FFI, снимки карт в ней появились лишь в сентябре5–8 дней, главный риск
Камера селфиCameraX, фронталкаUIKitView + AVCaptureSession (пример в документации JetBrains); Kamera 1.2 или FileKit — если хватит системного видоискателя, но рамка макета требует своего2–3 дня
Выбор фото, снимокPickVisualMedia, TakePicture, FileProvider, BitmapPHPicker, UIImagePickerController, JPEG из UIImage (FileKit 0.16 закрывает всё это)1–2 дня
РазрешенияcheckSelfPermission, «в настройки» через IntentAVCaptureDevice, CLLocationManager, openSettingsURL; свои expect/actual (moko-permissions собран на Kotlin 1.9)1 день
Геолокация, геокодерLocationManager, GeocoderCLLocationManager, CLGeocoder; Photon — общий код1–2 дня
Окно макетаActivity, WindowManagerUIScreen; инсеты: верх max(54, safe-area), низ 341 день
Deep linksintent-filterURL scheme в Info.plist → навигация0,5 дня
Токены, флагиDataStore + ContextDataStore 1.2.1 тот же; токены — Keychain (решение при переносе)0,5–1 день
ТелеметрияSentry Android, OkHttp-интерцепторsentry-kotlin-multiplatform 0.27 (0.x, «не тестируется полностью»), крошки запросов — плагин Ktor, dSYM через sentry-cli1–2 дня
Точка входаApplication, ActivityMainViewController + Swift-обёртка, AppDelegate0,5 дня

Замена библиотек

СейчасНа обеих платформахВерсия на 24.09.2026
Retrofit + OkHttpKtor client (Darwin на iOS, OkHttp на Android), WebSocket-плагин; 46 ручек переписать3.6.0
Coil 2.7 + SVGCoil 3 + coil-svg + coil-network-ktor33.6.3
ZXingqrose1.3.0
Sentry Androidsentry-kotlin-multiplatform0.27.0
java.timekotlinx-datetime0.8.0
DataStore 1.1.1DataStore (KMP с 1.1)1.2.1
navigation, lifecycleorg.jetbrains.androidx.*2.9.2 / 2.11.0
MapLibre Android 11.5.2на Android оставить; iOS — MapLibre Native iOS через UIKitView6.31.0
material-icons-extendedубрать: не используется, в CMP заморожена на 1.7.3—
Paparazziоставить для Android; на iOS — съёмка симулятора или Roborazzi для iOS (экспериментальный)1.75.0

Честная граница паритета и ограничения на iOS

4Как проверять «пиксель в пиксель» на iOS

  1. Экран. Симулятор iPhone 16e или 17e: 390×844 при масштабе 3 = 1170×2532 — ровно рендеры Figma ×3 (624 файла) и эталон Android. Никаких пересчётов.
  2. Съёмка. Как сейчас на Android: debug-сборка принимает аргумент запуска с guid кадра (и высотой для высоких кадров), снимок — xcrun simctl io … screenshot. Скрипт съёмки получает второй бэкенд «ios» рядом с «adb».
  3. Сравнение. Те же diff.py, attrib.py, inkcheck.py, textpos.py: на входе два PNG 1170×2532. Та же маска системного хрома. Прогон — на макмини.
  4. Порог — тот же 0,3 % по геометрии. Класс остатка «растеризация глифов CoreText» заводится только после раскладки красных пикселей по слоям (К-16).
  5. Порядок — те же десятки S01…S95; сначала кит, потом экраны. Ожидание: зелёные кадры Android зелёные и на iOS, красные (карта, размытие листа регистрации, перевыборка фото) красные и там.
  6. Ручная проверка по правилу от 14.09: каждый экран руками в симуляторе и на живом iPhone.

5Пробная сборка на макмини 24.09

Проверялось: способен ли макмини собрать Compose Multiplatform под iOS и компилируются ли под iOS приёмы, на которых держится наша вёрстка. Проект вне рабочего дерева (~/cmp-smoke): Kotlin 2.4.20, Compose Multiplatform 1.12.1, navigation 2.9.2, lifecycle 2.11.0.

В пробе: Modifier.blur; GraphicsLayer + BlurEffect с тройной отрисовкой (как стекло PdGlassBackdrop); BlendMode.Plus; на Skia-канвасе — битмап ALPHA_8, размытие MaskFilter.makeBlur, режимы PLUS и DST_OUT (модель FigDraw); контур из pathData ноды; поле ввода с кириллицей и ₽; список; два экрана через NavHost.

  • сборка фреймворк под iOS-симулятор — успех за 1 мин 27 с; тулчейн Kotlin/Native скачался сам и работает с Xcode 26.6.
  • запуск обёртка Xcode собралась, приложение установлено и запущено на iPhone 17e (iOS 26.5), снимок 1170×2532. Всё заложенное рисуется, падений нет. Debug-сборка — 55 МБ.
  • ловушка 1 Compose на iOS падает на старте без ключа CADisableMinimumFrameDurationOnPhone = true в Info.plist (нужен для 120 Гц) — первые два запуска упали на этом.
  • ловушка 2 запись слоя внутри drawWithCache в Compose 1.12 требует явной плотности — та же строка есть в GlassBackdrop.kt; nativeCanvas с 1.11 устарел, вместо него skiaCanvas; Gradle 8.13 для Kotlin 2.4.20 устарел.
Снимок пробного приложения Compose Multiplatform на симуляторе iPhone 17e: заголовок «CMP smoke · iOS 26.5», размытый синий блок, стеклянная плашка, тёмный прямоугольник с режимами наложения, фиолетовый контур, поле ввода с кириллицей, кнопка «Дальше» и список строк
Снимок симулятора iPhone 17e, 1170×2532, уменьшен. Сверху вниз: blur, стекло через GraphicsLayer, PLUS и DST_OUT на Skia, контур из pathData, поле ввода, кнопка навигации, список.

6Apple: TestFlight и App Store

Сборка и загрузка

App Review — что должно быть в приложении

Россия

572-ФЗ. «Селфи против фото профиля» считается биометрической аутентификацией: коммерческой организации нельзя хранить и сверять биометрию вне ЕБС или аккредитованной системы, штрафы до 2 млн ₽. Нужен юрист до выпуска в стор; касается и Android.

TestFlight

Что нужно от владельца и DevOps

  1. Принять обновлённое Program License Agreement (18.08.2026) — без него нельзя создать запись приложения и загружать сборки.
  2. Зарегистрировать App ID — предлагаю зеркалить Android: com.podushe.app.dusha, .dushalove, com.podushe.app; capability Push включить заранее.
  3. Создать запись «По душе» в App Store Connect и профили под каждый bundle id.
  4. Дать исполнителю роль Admin или App Manager. Ключ App Store Connect API уже есть на маке.
  5. Ключ APNs — только вместе с пушами.
  6. Метаданные ревью: политика, контакты, демо-номер (сервер), метки конфиденциальности, возрастная анкета, заметка «чем отличается».
  7. Юрист: 572-ФЗ, согласие на ПДн, уведомление РКН. Финансы: чем платить продление.

7Пуши

В Android «По душе» пушей нет, сервер умеет только FCM, а очередь пушей на стенде мёртвая с 17.09. Первая iOS-версия выходит без пушей — это и есть паритет.

Когда пуши появятся (задача с сервером): на iOS слать напрямую в APNs — ключ .p8, HTTP/2, библиотека aioapns для Python; серверу нужно хранить токен с платформой. Firebase на iOS даёт только обёртку над тем же APNs и добавляет зависимость от googleapis, которые в РФ периодически режут; APNs пока не блокируют.

8План по этапам

ЭтапЧто делаетсяЗависит отОценка
Э0. Подготовка без переезда файловВ текущем модуле: платформенное — за фасады, убрать неиспользуемое, java.time → kotlinx-datetime, Retrofit → Ktor, Coil 2 → 3, ZXing → qrose, ресурсы Compose. Каждая замена — отдельный коммит с пересъёмкой затронутых кадров. Соседние сессии продолжают работать в тех же файлахрешение владельца «идём CMP»2–3 недели
Э1. ДвигательKotlin 2.4.20, Compose 1.12, navigation 2.9, lifecycle 2.11; пересъёмка и сравнение всех 623 кадров на Android; разбор сдвиговЭ01 неделя
Э2. Переезд в KMPПеренос файлов одним коммитом в согласованное окно (день-два без чужих правок), iosApp от xcodegen, флейворы Xcode, PrivacyInfo, иконка, Info.plist. Android-сборка прежняяЭ1, окно тишины3–5 дней
Э3. iOS-узлыFigDraw на Skia, нативный канвас 8 экранов, карта, камера, фото, разрешения, гео, окно и инсеты, deep links, Sentry, хранилищеЭ23–4 недели
Э4. Сверка на iOSСъёмка по guid через simctl, diff по десяткам S01…S95, разбор красных, ручной проход в симуляторе и на айфонеЭ32–3 недели
Э5. TestFlightApp ID, запись в ASC, профили, архив, загрузка, внутренние тестерыдоступы (раздел 6), Э3 — можно раньше сверки2–3 дня
Э6. App StoreУдаление аккаунта и демо-номер (сервер), политика, анкеты, скриншоты, юрист по 572-ФЗ, внешние тестеры, ревьювладелец, сервер, юристне оцениваю

Итого до сверенной внутренней сборки в TestFlight — 8–12 недель. Ускоряет: Э0 идёт в текущей ветке без остановки остальной работы; Э5 не ждёт Э4. Замедляет: карта и неожиданные сдвиги текста после Э1.

9Риски

РискЧем снимаем
Переезд файлов ломает параллельные веткиЭ0 без переноса файлов; перенос одним коммитом в объявленное окно; чужие ветки перебазируются одним rebase
Обновление Compose сдвигает текст на AndroidЭ1 отдельно, до переезда, с полной пересъёмкой
Карта на iOS: библиотека beta, FFI нестабиленUIKitView + MapLibre Native iOS 6.31 (стабильный SDK); Android не трогаем; маркеры рисуем Skia — общий код
Текст на iOS ±1 px, эмодзи другиеМетрика «без текста» + inkcheck; класс остатка только после раскладки по слоям
Sentry KMP 0.xМинимальный набор; при проблемах — Sentry Cocoa напрямую из Swift
104 МБ картинок макета, размер бинарникаНа iOS то же, что на Android; сжатие отдельной задачей; релизный размер проверить на Э5
572-ФЗ и селфиЮрист до стора; технически селфи на iOS повторяет Android
App Store в РФ с перебоями; оплата членстваTestFlight по приглашению; внутренние тестеры; зарубежная карта, проверить дату продления
Xcode 27 в 2027Держать 26.x, следить за поддержкой в Kotlin/Native

10Вопросы владельцу

  1. Идём путём общего кода (Compose Multiplatform)?сервер не нужен
    Тогда iOS получает ту же вёрстку, и все правки экранов делаются один раз. Другой путь — писать iOS отдельно на Swift: дольше и без гарантии совпадения.
  2. Когда можно окно тишины на 1–2 дня для переноса файлов?сервер не нужен
    В это время в android-events никто не правит экраны (этап Э2).
  3. Статус-бар на iOS — настоящий?сервер не нужен
    У айфонов с «островком» он на 5 пунктов выше макетных 54. Предлагаю max(54, системный): содержимое не уедет под островок, на iPhone 16e и 14 совпадёт с макетом. Если не против — делаю так.
  4. Демо-номер для проверяющего Apple с постоянным кодомнужен сервер
    Сейчас на проде код выдаётся любому номеру — закрыть и оставить один служебный номер. Вадим.
  5. Удаление аккаунта из настроек должно реально удалять данныенужен сервер
    Apple проверяет это при ревью.
  6. Селфи-верификация и закон о биометриипо итогам — сервер
    По 572-ФЗ её нельзя делать самим без аккредитации. Нужен юрист; решение влияет и на Android.
  7. Аккаунт Apple: кто Account Holder, продление, оплатасервер не нужен
    Нужно принять новое соглашение от 18.08.2026, создать App ID и запись «По душе».
  8. Пуши в iOS-версиинужен сервер
    Не делаем до появления пушей в Android; когда решите — отдельная задача.
  9. Заводить карточку в трекере «iOS «По душе»» с этапами Э0–Э6?сервер не нужен
    Сейчас такой карточки нет.

11Источники

Код и инфраструктура: android-events/app/build.gradle.kts, спека паритета v3.20, docs/figma/REPORT.md, ios/README.md, docs/internal/10-mobile.md, база знаний (клиенты, доступы), трекер DUSHA-4, DUSHA-A-17, DUSHA-M-1, LYUBVI-39; макмини (проверки 24.09.2026); Maven Central и Google Maven; пробная сборка ~/cmp-smoke. Полный текст с таблицами — docs/41-ios-podushe-research-2026-09-24.md в репозитории.

Compose Multiplatform и библиотеки
Apple и Россия