Implementing Scheduled Notifications in Mobile Apps
Scheduled Notifications are notifications with exact delivery time set in advance. From simple "remind me in 30 minutes" to daily reminders at user's specific time. Technically, either local notifications (client schedules) or server delivery via FCM/APNs by schedule.
When to Choose Which
Local notifications — if time is tied to device and doesn't change from server. Habit tracker, medication reminder, alarm event.
Server scheduled — if centralized logic needed: marketing campaigns by schedule, event reminders for all participants, deadline notification considering user's timezone.
Server Delivery by Schedule
OneSignal supports scheduled delivery directly in API:
{
"app_id": "YOUR_APP_ID",
"include_aliases": { "external_id": ["user_44521"] },
"contents": { "en": "Team meeting in 15 minutes" },
"send_after": "2024-03-20 14:45:00 UTC",
"delayed_option": "timezone",
"delivery_time_of_day": "9:00AM"
}
delayed_option: "timezone" — deliver at specified time considering each recipient's timezone. Useful for "good morning" campaigns.
For own backend — cron job or task via Celery/BullMQ:
// Node.js + BullMQ
const notificationQueue = new Queue('notifications', { connection: redis });
async function scheduleNotification(userId, content, sendAt) {
const delay = sendAt.getTime() - Date.now();
await notificationQueue.add(
'send_push',
{ userId, content },
{ delay, attempts: 3, backoff: { type: 'exponential', delay: 5000 } }
);
}
attempts: 3 with exponential backoff — mandatory. FCM sometimes returns 503, need retry.
Local Scheduling on iOS
// Reminder every day at 8:00
func scheduleHabitReminder(habitId: String, name: String) {
let content = UNMutableNotificationContent()
content.title = name
content.body = "Don't forget to mark as completed"
content.sound = .default
content.userInfo = ["habit_id": habitId]
var components = DateComponents()
components.hour = 8
components.minute = 0
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
let request = UNNotificationRequest(
identifier: "habit-\(habitId)",
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request) { error in
if let error { print("Schedule failed: \(error)") }
}
}
// Change reminder time (remove old, add new)
func rescheduleReminder(habitId: String, newHour: Int, newMinute: Int) {
UNUserNotificationCenter.current()
.removePendingNotificationRequests(withIdentifiers: ["habit-\(habitId)"])
// ... create new request with updated components
}
Local Scheduling on Android with WorkManager
AlarmManager is precise but doesn't survive reboot. WorkManager survives reboot but timing is approximate (±15 minutes on Android 12+ due to Doze). Choice depends on accuracy requirements.
// WorkManager — for non-critical timing reminders
fun scheduleHabitReminder(habitId: String, reminderHour: Int, reminderMinute: Int) {
// Calculate delay until next trigger
val now = Calendar.getInstance()
val target = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, reminderHour)
set(Calendar.MINUTE, reminderMinute)
set(Calendar.SECOND, 0)
if (before(now)) add(Calendar.DAY_OF_YEAR, 1)
}
val delayMs = target.timeInMillis - now.timeInMillis
val workRequest = OneTimeWorkRequestBuilder<ReminderWorker>()
.setInitialDelay(delayMs, TimeUnit.MILLISECONDS)
.setInputData(workDataOf(
"habit_id" to habitId,
"next_reminder_hour" to reminderHour,
"next_reminder_minute" to reminderMinute
))
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork("habit-$habitId", ExistingWorkPolicy.REPLACE, workRequest)
}
// In Worker — show notification and schedule next
class ReminderWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val habitId = inputData.getString("habit_id") ?: return Result.failure()
showNotification(habitId)
// Schedule next day
scheduleHabitReminder(
habitId,
inputData.getInt("next_reminder_hour", 8),
inputData.getInt("next_reminder_minute", 0)
)
return Result.success()
}
}
ExistingWorkPolicy.REPLACE — if user changed reminder time, old task is replaced with new.
Managing Reminders in UI
User should see scheduled reminders and manage them. On iOS:
// Load all scheduled reminders
func loadPendingReminders() async -> [ScheduledReminder] {
return await withCheckedContinuation { continuation in
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
let reminders = requests.compactMap { ScheduledReminder(from: $0) }
continuation.resume(returning: reminders)
}
}
}
Display in list with ability to edit time or delete. When deleting — removePendingNotificationRequests + remove from WorkManager (Android).
Timeline
Implementing scheduled notifications with UI for schedule management, supporting reboot on Android and 64 notification limit on iOS — 3–5 working days. If adding server scheduling via OneSignal or custom queue — another 2–3 days.







