Реалізація Silent Push Notifications у мобільному додатку
Silent push — сповіщення без звуку, без банера, без взаємодії користувача. Приходить у фоні, будить додаток, дає йому кілька секунд на виконання коду. Використовується для фонової синхронізації контенту, інвалідації кеша, оновлення badge counter без відкриття додатку.
iOS: Background Fetch через Silent Push
На iOS silent push вимагає двох речей: флага content-available: 1 у payload та включеного Background Mode «Remote notifications» у Xcode Capabilities.
Payload APNs:
{
"aps": {
"content-available": 1
},
"sync_type": "messages",
"last_known_id": "msg_8823"
}
Без alert, без sound — чистий фоновий виклик. iOS викличе:
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
guard let syncType = userInfo["sync_type"] as? String else {
completionHandler(.noData)
return
}
Task {
do {
let hasNewData = try await SyncManager.shared.sync(type: syncType)
completionHandler(hasNewData ? .newData : .noData)
} catch {
completionHandler(.failed)
}
}
}
Критичний момент: iOS дає близько 30 секунд на виконання. Якщо за цей час completionHandler не викликано — iOS примусово завершить фонову задачу. Крім того, iOS не гарантує доставку silent push при низькому заряді батареї (Low Power Mode) або коли користувач примусово закрив додаток.
Примусове закриття — головна біль. Force quit через iOS task switcher повністю блокує silent push до наступного ручного відкриття додатку. Це документовано, обійти неможливо.
Android: FCM Data Message та WorkManager
На Android немає прямого аналога «silent push» — є FCM Data Message, який завжди потрапляє в FirebaseMessagingService.onMessageReceived незалежно від стану додатку (за умови, що він не убитий системою Doze).
class AppFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
val syncType = message.data["sync_type"] ?: return
// Запускаємо WorkManager задачу — коротку, з гарантією виконання
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(workDataOf("sync_type" to syncType))
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(applicationContext).enqueue(workRequest)
}
}
setExpedited() — запит на немедленне виконання. Android 12+ вимагає setForegroundAsync всередину expedited worker або foreground service, інакше ANR при довгій операції.
Doze Mode — Android обмежує фонову активність на пристроях без зарядки. FCM використовує high-priority повідомлення для обходу Doze, але потрібно явно вказати priority: "high" при відправці через FCM:
{
"message": {
"token": "device_fcm_token",
"android": {
"priority": "HIGH"
},
"data": {
"sync_type": "messages",
"payload": "{...}"
}
}
}
Використання для badge counter
Популярний кейс — оновити цифру на іконці додатку без показу сповіщення:
// iOS — через silent push
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
if let badge = userInfo["badge"] as? Int {
UNUserNotificationCenter.current().setBadgeCount(badge) { _ in }
}
completionHandler(.newData)
}
На Android badge оновлюється через ShortcutBadger (стороння бібліотека) або через NotificationManagerCompat з setNumber(). Єдиного API немає — кожен лончер (Samsung, Xiaomi, Huawei) має свій механізм.
Обмеження та quota
iOS 13+ запровадив BGTaskScheduler та background processing quota. Якщо додаток занадто часто запитує фонове виконання та не приносить користі користувачу (за думкою iOS) — система починає throttle-ити виклики. Повернення .noData від completionHandler у відповідь на silent push, де даних немає — важливо для коректної роботи системи квот.
Терміни
Налаштування silent push для iOS (Background Modes, handler) та Android (FCM Data Message + WorkManager), покриття граничних випадків (Doze, force quit, quota) — 3–5 робочих днів.







