תזמון הודעות — אחת מאותן משימות שנראית פשוטה עד שנתקלים במגבלות הפלטפורמה. ב-iOS — מגבלה של 64 הודעות פעילות, ב-Android — סיכון שאובדות תזכורות לאחר אתחול. בנינו מערכות כאלה במשך 7 שנים ואנחנו יודעים איך לעקוף את המלכודות האלה. במאמר זה נפרק אילו טכנולוגיות לבחור ואיך להימנע מטעויות נפוצות.
פעם לקוח ביקש להוסיף תזכורות יומיות לאפליקציית מעקב הרגלים. על פני השטח — משימה טריוויאלית. אבל במהלך בדיקות ב-iOS, האפליקציה הגיעה במהירות למגבלת 64 ההודעות, וב-Android התזכורות הפסיקו לעבוד לאחר אתחול המכשיר. נאלצנו לעצב מחדש את הארכיטקטורה: ב-iOS — שימוש דינמי חוזר במזהים, ב-Android — מעבר מ-AlarmManager ל-WorkManager. החוויה עלתה בכמה לילות ללא שינה, אבל בסופו של דבר המערכת הפכה ליציבה.
מתי לבחור מה
הודעות מקומיות — אם הזמן קשור למכשיר המשתמש ואינו משתנה מהשרת. מעקב הרגלים, תזכורת תרופות, שעון מעורר לאירוע.
תזמון בצד השרת — אם יש צורך בלוגיקה מרכזית: קמפיינים שיווקיים לפי לוח זמנים, תזכורת על אירוע לכל המשתתפים, הודעת דדליין תוך התחשבות באזור הזמן של המשתמש.
| מקומי | צד השרת | |
|---|---|---|
| תלות ברשת | לא נדרשת | נדרשת |
| ניהול | במכשיר | בשרת |
| מקסימום הודעות | 64 (iOS) / ללא הגבלה (Android) | ללא הגבלה |
| חזרתיות | טריגרים של לוח שנה/זמן | Cron / תור משימות |
| דוגמה | תזכורת "קום" כל יום ב-9:00 | קמפיין "ההצעה מסתיימת בעוד שעה" לכל המשתמשים |
איך לבחור בין הודעות מקומיות להודעות בצד השרת?
אם ההודעה קשורה לפעולת משתמש (למשל, הוא הגדיר את התזכורת בעצמו) — לכו על מקומי. אם ההודעה יזומה על ידי השרת (הזמנה חדשה, דדליין, קמפיין המוני) — לכו על צד השרת. גישה היברידית: הודעה מקומית יכולה להיות "מסונכרנת" עם השרת באמצעות push stub.
תזמון בצד השרת
OneSignal תומך במשלוח מתוזמן ישירות ב-API:
{
"app_id": "YOUR_APP_ID",
"include_aliases": {
"external_id": ["user_44521"]
},
"contents": {
"ru": "Встреча с командой через 15 минут"
},
"send_after": "YYYY-MM-DD HH:MM:SS UTC",
"delayed_option": "timezone",
"delivery_time_of_day": "9:00AM"
}{ "app_id": "YOUR_APP_ID", "include_aliases": { "external_id": ["user_44521"] }, "contents": { "ru": "Встреча с командой через 15 минут" }, "send_after": "YYYY-MM-DD HH:MM:SS UTC", "delayed_option": "timezone", "delivery_time_of_day": "9:00AM" } — משלוח בשעה שצוינה ביום תוך התחשבות באזור הזמן של כל נמען. שימושי לקמפיינים של "בוקר טוב".
לשרת מותאם אישית — cron job או תור משימות באמצעות 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
}
}
);
}delayed_option: "timezone" עם backoff אקספוננציאלי — חובה. FCM לפעמים מחזיר 503, צריך לנסות שוב.
תזמון מקומי ב-iOS
// Напоминание каждый день в 8:00
func scheduleHabitReminder(habitId: String, name: String) {
let content = UNMutableNotificationContent()
content.title = name
content.body = "Не забудьте отметить выполнение"
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)")
}
}
}
// Изменить время напоминания (удалить старое, добавить новое)
func rescheduleReminder(habitId: String, newHour: Int, newMinute: Int) {
UNUserNotificationCenter.current()
.removePendingNotificationRequests(withIdentifiers: ["habit-\(habitId)"])
// ... создаём новый request с обновлёнными компонентами
}Apple ממליצה לא לחרוג מ-64 הודעות מתוזמנות לכל אפליקציה (UNUserNotificationCenter).
למה WorkManager עדיף על AlarmManager ב-Android?
AlarmManager מדויק אבל לא שורד אתחול. WorkManager שורד אתחול אבל זמן הביצוע משוער (±15 דקות ב-Android 12+ בגלל Doze). הבחירה תלויה בדרישות הדיוק. לתזכורות שבהן הדיוק לא קריטי (למשל, "שתו מים"), WorkManager הוא פתרון אמין.
// WorkManager — для некритичных по времени напоминаний
fun scheduleHabitReminder(habitId: String, reminderHour: Int, reminderMinute: Int) {
// Вычисляем delay до следующего срабатывания
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)
}
// В Worker — показываем уведомление и планируем следующее
class ReminderWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val habitId = inputData.getString("habit_id") ?: return Result.failure()
showNotification(habitId)
// Планируем следующий день
scheduleHabitReminder(
habitId,
inputData.getInt("next_reminder_hour", 8),
inputData.getInt("next_reminder_minute", 0)
)
return Result.success()
}
}// 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 } } ); } — אם המשתמש משנה את שעת התזכורת, המשימה הישנה מוחלפת בחדשה.
ניהול תזכורות בממשק המשתמש
המשתמש צריך לראות תזכורות מתוזמנות ולהיות מסוגל לנהל אותן. ב-iOS:
// Загрузить все запланированные напоминания
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)
}
}
}הצגה ברשימה עם אפשרות לערוך שעה או למחוק. בעת מחיקה — attempts: 3 + הסרה מ-WorkManager (Android).
טעויות נפוצות ואיך להימנע מהן
רשימת בדיקה: מה לבדוק לפני העלייה לאוויר
- לוודא ש-iOS לא חורגת ממגבלת 64 ההודעות. שימוש חוזר במזהים.
- ב-Android, לבדוק לאחר אתחול המכשיר.
- להודעות חוזרות, להשתמש ב-WorkManager עם תזמון מחדש בתוך ה-Worker.
- לבדוק במצב Doze (Android) ובמצב חיסכון בחשמל (iOS).
- לוודא שאזור הזמן מטופל כראוי.
מה כוללת העבודה
פיתוח מפתח לתזמון הודעות כולל:
- עיצוב ארכיטקטורת הודעות (מקומי/שרת/היברידי)
- הטמעת מתזמן ב-iOS (UNUserNotificationCenter) וב-Android (WorkManager או AlarmManager)
- אינטגרציה עם תור שרת (BullMQ, Celery) או OneSignal
- ממשק משתמש לניהול לוח זמנים (רשימה, הוספה, עריכה, מחיקה)
- בדיקות על מכשירים אמיתיים: מצב Doze, אתחול, שינוי אזור זמן
- תיעוד על גישה ותפעול
אנחנו מבטיחים פעילות יציבה של הודעות — מוכחת ביותר מ-50 פרויקטים. עלות הפיתוח המפתח תלויה במורכבות ונקבעת לאחר ניתוח. צרו קשר כדי לזהות את המקרה שלכם ולקבל הערכת עלות.
לוח זמנים
הטמעת תזמון הודעות עם ניהול לוח זמנים בממשק המשתמש, תמיכה באתחול ב-Android וטיפול במגבלת 64 ההודעות ב-iOS — 3–5 ימי עבודה. אם מתווסף תזמון בצד השרת באמצעות OneSignal או תור מותאם אישית — עוד 2–3 ימים. קבלו ייעוץ והערכת עלות מדויקת לפרויקט שלכם — פנו אלינו.







