Как я написал 4-мегабайтный блокировщик залипания в телефоне под iOS, победил баги Screen Time API и почему отказался от уведомлений и экранов оплаты
Как я написал 4-мегабайтный блокировщик залипания в телефоне под iOS, победил баги Screen Time API и почему отказался от уведомлений и экранов оплаты
История о том, как борьба с дофаминовыми ловушками вылилась в разработку собственного нативного приложения Scroll Brake, почему популярные аналоги требуют странные разрешения и как обойти неочевидные грабли в Apple Screen Time API.
Ловушка на 45 минут
Я очень трепетно отношусь к продуктивности. За годы работы я собрал собственную систему управления временем и энергией, которая отлично держит структуру дня и помогает закрывать задачи.
Но бывают моменты, когда просто на автомате берешь телефон в руки и начинаешь залипать в рилсы. Одно короткое видео, второе, третье — лента постоянно подсовывает что-то интересное. Вроде только зашел, а очнулся — прошло уже минут сорок или целый час. Алгоритмы соцсетей созданы лучшими инженерами мира с одной целью: удерживать внимание любой ценой. Каждое следующее видео интереснее предыдущего, и мозг проваливается в бесконечную дофаминовую петлю.
Я не собирался писать свое приложение. Как разработчик и фаундер я ценю время и предпочитаю использовать готовые решения. Поэтому я пошел в App Store искать существующие инструменты.
Что не так с современным рынком блокировщиков?
Я протестировал ключевых игроков ниши — Opal, OneSec, ScreenZen, Refocus и стандартный Screen Time от Apple. И почти сразу наткнулся на вещи, которые вызвали у меня откровенное недоумение:
Парадокс уведомлений и приватности
Первое, что делают почти все такие приложения — сразу просят доступ к уведомлениям и аналитике. Зачем сервису, который призван убрать телефон из рук, слать мне пуши? Как выяснилось уже во время разработки, дело в давнем системном ограничении Apple (дальше в технической части разберу подробнее).
Агрессивные экраны оплаты
Ты еще не успел открыть главный экран и понять, решает ли сервис твою проблему, а тебе уже навязчиво выкатывают подписку с «бесплатным триалом», расчет в котором явно строится на том, что пользователь просто забудет отписаться. Как предприниматель я понимаю эту воронку, но как пользователя меня передергивает от такого подхода.
Бесполезность стандартного Apple Screen Time
Встроенный лимит от Apple не работает психологически. Когда всплывает стандартный системный экран блокировки, рука на автомате жмет «Игнорировать лимит на 15 минут». Здесь нет когнитивного барьера — действие совершается машинально.
Тратить время на перебор десятков клонов с одинаковыми экранами подписки не хотелось. Я понял: быстрее и честнее сделать ультра-легкий нативный инструмент под себя.
Принципы Scroll Brake: 4 МБ, без трекеров и подписок
В NovaSynapse мы придерживаемся принципа «Everything you need. Nothing you don’t». Приложение должно решать задачу, не жрать батарею и уважать границы пользователя.
Так родился Scroll Brake:
- Вес всего ~4 МБ: Никаких тяжелых фреймворков, веб-вью и раздутых зависимостей.
- 100% Privacy-First: Никакой регистрации, никаких баз данных, никаких внешних серверов. Всё исполняется локально на устройстве через нативные API iOS.
- Когнитивное трение вместо тупого запрета: Чтобы продлить время в соцсетях, нужно включить префронтальную кору — например, решить математический пример. Это сбивает автопилот.
- Антидофаминовый UX вместо геймификации: После правильного решения примера я намеренно не показываю экран с фейерверками, конфетти и не поздравляю с решением. Подкармливать мозг дешевым дофамином за шаг к соцсетям — абсурд. Экран намеренно сделан максимально скучным: таймер разблокирован, а для быстрого возврата используется стандартная кнопка перехода в предыдущее приложение в левом верхнем углу (в iOS из соображений приватности хост-приложение не может знать, из какого именно сервиса сработал лимит, поэтому возврат происходит через системный breadcrumb).
- Монетизация: До 3 правил приложение бесплатное (мне для себя хватает ровно одного правила: категория «Соцсети» не более 5 минут в день). Для тех, кому нужно больше, сделал разовую покупку навсегда (Lifetime) вместо обязательных подписок.
Как велась разработка: 4 дня, AI и инструменты Xcode
Приложение я писал с помощью AI — использовал Antigravity с моделями Gemini (3.1 Pro и Flash).
Раньше я уже пробовал писать мобильные приложения на Flutter и нативно, но до эпохи AI-ассистентов на это уходило куда больше времени. Здесь же всё получилось собрать довольно быстро. Слепо принимать сгенерированный код, конечно, нельзя: системные расширения у Apple капризные, поэтому код-ревью я делал сам, чтобы ничего не крашилось и не текло по памяти. Но в целом разработка шла легко и заняла буквально 3–4 чистых рабочих дня со всеми тестами на эмуляторе и реальном телефоне:
- Иконка за 15 минут: Вместо ручной отрисовки десятка PNG-ассетов я использовал Icon Composer из Developer Tools в Xcode. С его помощью векторные слои автоматически адаптируются под светлую, темную и тонированную темы iOS.
- App Store Review < 24 часов: Поскольку в приложении нет скрытого трекинга, сторонних SDK и сбора персональных данных, модерация Apple пропустила билд на следующий же день после отправки.
Инженерный Deep-Dive: Скрытые грабли Screen Time API
А теперь самое интересное — техническая изнанка работы со связкой ManagedSettings, DeviceActivity и FamilyControls. Если вы решите разрабатывать утилиту под Screen Time, вы неизбежно упретесь в три системных барьера. Вот как я их решил:
Почему конкуренты просят Push-уведомления? (iOS < 26.5 vs iOS 26.5+)
Многие пользователи ругают приложения за спам уведомлениями, но мало кто знает, что разработчики долгое время были заложниками архитектуры Apple.
- Проблема старых версий:
Когда приложение блокируется, система показывает системный экран защиты — ShieldActionExtension. В нем пользователь нажимает кнопку «Продлить». Но расширение исполняется в урезанном процессе, где вызов UIApplication.shared.open запрещен на уровне компилятора и системы. Enum ShieldActionResponse поддерживал только сценарии .close, .defer и .none. Открыть хост-приложение для ввода кода или решения примера напрямую было невозможно!
- Костыль индустрии:
Единственным способом перебросить пользователя в приложение была отправка локального push-уведомления (UNUserNotificationCenter). Пользователю требовалось вручную свайпнуть шторку или тапнуть баннер уведомления, чтобы открыть приложение. Именно поэтому почти все аналоги просят разрешение на нотификации.
- Мое решение в iOS 26.5+:
Apple наконец добавила нативный кейс ответа ShieldActionResponse.openParentalControlsApp. При его возврате система сама мгновенно переносит пользователя в главное приложение к экрану решения математического примера (MathPuzzleView):
// ShieldActionExtension.swift
defaults?.set(true, forKey: "launchedFromShieldAction")
completionHandler(.openParentalControlsApp)
Я зафиксировал минимальную версию проекта на современном таргете (IPHONEOSDEPLOYMENTTARGET = 26.5), полностью исключив костыли с push-уведомлениями из архитектуры.
Запрет на прямое чтение статистики использования (Privacy Sandbox)
Меня часто спрашивают: «Почему приложение не показывает детальный поминутный график активности внутри каждого приложения в кастомной аналитике?»
Ответ прост: Apple архитектурно изолирует данные об использовании от хост-приложения.
- Доступ к массиву
DeviceActivityResultsпредоставляется исключительно расширению DeviceActivityReportExtension, которое исполняется в изолированном системном процессе. - Хост-приложение встраивает лишь визуальный контейнер DeviceActivityReport. Он рендерится удаленно (out-of-process remote rendering) и проецируется в интерфейс приложения.
- Данные не покидают песочницу расширения — приложение не может сохранить их в локальную БД, преобразовать в массивы или отправить по сети.
Официальная позиция Apple прямо фиксирует это ограничение:
"To protect user privacy, the DeviceActivityReport extension runs in a sandbox, and its data doesn’t leave the extension. Instead, the extension creates and returns views that your app displays."
- Следствие для мониторинга:
Отслеживание лимитов через DeviceActivityCenter строится исключительно на подписке на пороговые значения (DeviceActivityEvent.threshold). Система уведомляет расширение DeviceActivityMonitorExtension только в момент, когда порог достигнут (eventDidReachThreshold), не отдавая промежуточных значений использования.
- Почему возврат происходит по кнопке в левом верхнем углу:
В iOS хост-приложение принципиально не может знать, из какого именно приложения сработал щит блокировки — система не передает bundleIdentifier в целях защиты приватности. Из-за этого приложение не может программно переключить пользователя обратно в условный Instagram или TikTok. Возврат осуществляется через стандартную системную стрелку перехода в предыдущее приложение в левом верхнем углу статус-бара iOS.
Баги Screen Time API, которые пришлось устранять вручную
В ходе тестов я столкнулся с системными багами самой iOS, которые способны сломать любой блокировщик:
Event Caching Bug (Кэширование порогов в DeviceActivityCenter)
- Баг: При обновлении порога для существующего
DeviceActivityName(при продлении времени) iOS не сбрасывала внутренний триггер и игнорировала новыйthreshold. - Решение: Генерация уникального имени активности с токеном сессии
DeviceActivityName("limitс предварительным вызовом") stopMonitoring(). - Документация Apple: DeviceActivityCenter.startMonitoring).
deviceActivityCenter.stopMonitoring([previousActivityName])
let newActivity = DeviceActivityName("limit_\(UUID().uuidString)_\(sessionToken)")
try deviceActivityCenter.startMonitoring(newActivity, during: schedule, events: events)
Баг сброса на следующий день (Dual-Event Schedule Pattern)
- Баг: Расписание DeviceActivitySchedule с параметром
repeats: trueи уменьшенным порогом продления (например, 1 мин) на следующий день запускало тот же 1-минутный порог вместо суточной нормы. - Решение: Регистрация двух событий в одном расписании:
mainEvent(полный суточный лимит на завтра) иextEvent(с зашитой датой в имени события, активное только в текущие сутки до 23:59).
Краши OOM (Out-Of-Memory, код 11) в DeviceActivityReport
- Баг: Наличие реактивных таймеров в SwiftUI-иерархии вызывало непрерывную переоценку
DeviceActivityReport. Это провоцировало многократную агрегацию 7-дневных логов по XPC и приводило к утечке памяти и моментальному завершению процесса системой по OOM. - Решение: Полный отказ от реактивных таймеров в UI, фиксация фильтра отчета без постоянных мутаций и обновление данных строго по событиям жизненного цикла (
onAppear,willEnterForeground).
Как я настроил приложение для себя
С момента релиза Scroll Brake стал моим главным барьером от залипания в телефоне.
У меня это настроено так:
- Одно правило: категория «Social Networks» заблокирована.
- Базовый лимит: 5 минут в день.
- Продление: 5 минут за решение математического примера.
- Жесткий лимит: не более 5 продлений в сутки.
В итоге я физически не могу провести в соцсетях больше 25 минут в день. При этом мозг привык: чтобы зайти посмотреть присланный рилс, нужно решить задачу. Этого секундного трения оказывается достаточно, чтобы 8 раз из 10 просто положить телефон экраном вниз и вернуться к делам.