Уведомления macOS в приложении на Delphi
Уведомления рабочего стола помогают приложению сообщать о завершении операции, готовности файла, изменении состояния сервиса или наступлении запланированного события. В macOS эта функция реализуется через фреймворк UserNotifications, а приложение на Delphi может обращаться к нему из FireMonkey через Objective-C-мосты.
Для полноценной интеграции важно учитывать разрешения пользователя, особенности песочницы macOS, жизненный цикл приложения и различия между локальными уведомлениями и сообщениями от удалённого сервера. Правильно построенная архитектура позволяет сохранить общий код Delphi и вынести платформенную часть в отдельный модуль.
Как устроены уведомления в macOS
Система уведомлений macOS управляется объектом UNUserNotificationCenter. Через него приложение запрашивает разрешение, создаёт содержимое сообщения, планирует доставку и регистрирует категории действий. Уведомление может появиться в виде баннера, звукового сигнала или записи в центре уведомлений — конкретное поведение определяется настройками пользователя.
В локальном сценарии приложение само формирует событие и передаёт системе объект UNNotificationRequest. Запрос содержит идентификатор, содержимое уведомления и триггер. Триггер бывает временным, календарным или основанным на интервале. Например, для напоминания можно указать дату, а для периодической проверки — интервал в секундах.
Разрешение нужно запрашивать до планирования первого сообщения. Обычно приложение вызывает requestAuthorization с вариантами Alert, Sound и Badge. Ответ пользователя следует сохранить в логике приложения, однако повторный вызов не покажет диалог заново, если решение уже принято. При отказе можно открыть настройки приложения, но изменить разрешение программно нельзя.
Подготовка проекта Delphi
В проекте FireMonkey следует проверить целевую платформу macOS и используемую версию SDK. Названия доступных Delphi-модулей зависят от версии RAD Studio: в одних сборках присутствуют готовые объявления UserNotifications, в других требуется подключать Objective-C API через мосты или использовать собственный модуль-обёртку.
Практично разделить код на общий интерфейс и платформенную реализацию. Общий слой может содержать методы RequestPermission, ScheduleNotification и CancelNotification, а macOS-модуль будет преобразовывать параметры Delphi в объекты Apple. Такой подход упрощает поддержку Windows, Android и iOS, где механизм уведомлений отличается.
Упрощённая последовательность выглядит так:
Center := TUNUserNotificationCenter.OCClass.currentNotificationCenter;
Options := UNAuthorizationOptionAlert or
UNAuthorizationOptionSound or
UNAuthorizationOptionBadge;
Center.requestAuthorizationWithOptions(Options, Callback);
Имена классов и сигнатуры могут различаться в зависимости от подключённых заголовков. Перед использованием нужно проверить объявления в конкретной версии Delphi и убедиться, что вызовы выполняются с корректными Objective-C типами. Ошибки мостов часто проявляются как проблемы с управлением памятью или несовместимые параметры блоков обратного вызова.
Для фоновой работы необходимо учитывать модель macOS. Если приложение завершено, операционная система всё равно может доставить заранее запланированное локальное уведомление. Однако код Delphi не будет автоматически выполнен в момент показа баннера. Если требуется получить свежие данные с сервера, следует использовать push-уведомления или отдельный фоновый механизм macOS.
Локальные сообщения и параметры доставки
Содержимое уведомления создаётся объектом UNMutableNotificationContent. В нём задаются заголовок, основной текст, дополнительная строка, звук, значок и числовой бейдж. Идентификатор запроса должен быть уникальным, если сообщения должны существовать независимо. Для обновления конкретного напоминания удобно использовать постоянный идентификатор и повторно добавлять запрос с тем же значением.
| Задача | Объект или параметр | Практическое применение |
|---|---|---|
| Запросить разрешение | UNUserNotificationCenter |
Показ системного диалога при первом запуске |
| Задать текст | UNMutableNotificationContent |
Заголовок, сообщение, звук и бейдж |
| Доставить один раз | UNTimeIntervalNotificationTrigger |
Напоминание через заданный интервал |
| Доставить по расписанию | UNCalendarNotificationTrigger |
Событие в определённую дату и время |
| Отменить сообщение | Идентификатор запроса | Удаление ожидающего уведомления |
| Добавить кнопки | Категории уведомлений | Действия «Открыть», «Отложить» и другие команды |
Пример создания уведомления через Delphi может выглядеть следующим образом:
Content := TUNMutableNotificationContent.Create;
Content.title := StrToNSStr('Резервное копирование');
Content.body := StrToNSStr('Копирование файлов завершено');
Content.sound := TUNNotificationSound.OCClass.defaultSound;
Trigger := TUNTimeIntervalNotificationTrigger
.triggerWithTimeInterval(60, False);
Request := TUNNotificationRequest.OCClass
.requestWithIdentifier(
StrToNSStr('backup-finished'),
Content,
Trigger
);
Center.addNotificationRequest(Request, ErrorHandler);
В реальном проекте нужно проверять минимально допустимый интервал. Apple не разрешает некоторые слишком короткие значения для локальных уведомлений. Также полезно обрабатывать ошибку добавления запроса: она может возникнуть из-за некорректного триггера, отсутствующего разрешения или проблем преобразования строк.
Если уведомление должно повторяться, для календарного триггера устанавливается параметр repeats. Повторять событие через интервал можно, но для сложного расписания предпочтительнее использовать компоненты даты и времени. Временная зона и переходы на летнее время способны изменить ожидаемый момент доставки, поэтому расписание следует тестировать на реальных настройках системы.
Действия пользователя и обработчики
Одного показа баннера недостаточно, если уведомление должно запускать действие внутри приложения. Для этого регистрируют категорию с идентификатором и набором UNNotificationAction. Пользователь может нажать кнопку, выбрать команду из контекстного меню или открыть само сообщение. Каждое действие получает собственный идентификатор.
На уровне приложения назначается делегат центра уведомлений. Его методы обрабатывают показ сообщения, когда программа находится на переднем плане, и реакцию на выбор пользователя. Для Delphi это означает создание класса, реализующего Objective-C-протокол, либо использование готового мостового класса, если он предусмотрен библиотекой RAD Studio.
При обработке действия важно учитывать состояние приложения. Когда программа уже запущена, команду можно передать в основной поток FireMonkey и обновить форму. Если приложение было запущено нажатием на уведомление, параметры действия нужно сохранить до завершения инициализации главного окна. Обращение к визуальным компонентам из фонового callback может привести к нестабильности, поэтому обновление интерфейса выполняется через TThread.Queue или другой механизм главного потока.
Текст уведомления не должен быть единственным источником данных. После выбора действия приложение может повторно загрузить объект по его идентификатору из базы данных или сервера. Это защищает от устаревшего сообщения, удалённой записи или изменения состояния между моментом планирования и моментом нажатия.
Разрешения, песочница и проверка результата
Для распространения через Mac App Store приложение должно соответствовать требованиям sandbox. Настройки подписи и entitlements зависят от способа доставки, но уведомления следует проверять в подписанной сборке, а не только при запуске из среды разработки. Ошибка в профиле подписи может проявиться как отсутствие ожидаемого поведения даже при корректном коде Delphi.
Проверять результат нужно на нескольких уровнях. Сначала следует убедиться, что разрешение получено. Затем — посмотреть список ожидающих запросов через getPendingNotificationRequests, а для уже доставленных сообщений — историю центра уведомлений. В журнале полезно записывать идентификатор, время создания запроса, ответ системного API и текст ошибки.
Особое внимание требуется уделить режиму «Не беспокоить», фокусированию и настройкам конкретного приложения. macOS может принять запрос, но отложить визуальный показ или заменить его менее заметным способом. Это не всегда означает ошибку программы. На тестовом компьютере нужно проверить разрешения в «Системных настройках», звуковой профиль и разрешённый стиль оповещения.
Для автоматизированных тестов удобно отделить формирование контента от фактического вызова UserNotifications. В тестах можно проверять заголовок, тело, идентификатор и параметры триггера, не ожидая появления баннера. На физическом Mac дополнительно тестируются запуск после завершения приложения, повторное расписание, отмена, действия кнопок и переход по уведомлению.
Организация кода для кроссплатформенного проекта
В приложении, которое выпускается для Windows, macOS и мобильных платформ, не стоит размещать вызовы UNUserNotificationCenter в формах. Интерфейс уведомлений лучше определить в общем модуле, а реализацию подключать через условную компиляцию {$IF Defined(MACOS)}. На других платформах те же методы будут использовать нативные службы Android или iOS.
Параметры сообщения удобно описать отдельной записью: заголовок, текст, идентификатор, дата доставки, повторение и тип действия. Платформенный слой получает эту структуру и преобразует её в соответствующие системные объекты. Благодаря этому бизнес-логика не зависит от названий Apple API и легче покрывается тестами.
Следует заранее определить правила идентификаторов. Например, sync-result-42 может соответствовать конкретной операции синхронизации, а calendar-event-2025-04-01 — запланированному событию. При отмене приложение использует тот же идентификатор, а при изменении расписания заменяет существующий запрос. Неуправляемое создание случайных идентификаторов приводит к накоплению устаревших уведомлений.
Такой слой также упрощает переход от локальных сообщений к удалённым push-уведомлениям. Содержимое и обработчик действий могут оставаться общими, тогда как доставка через APNs, регистрация токена устройства и серверная отправка реализуются отдельно. В результате Delphi-приложение сохраняет единый пользовательский сценарий, а системные различия остаются внутри платформенного модуля.