Implementing Local Notifications in Mobile Apps
Local notifications — the only type that doesn't require a server at all. The app itself schedules them through the system API: at a specific time, by calendar trigger, or on geofence entry. Typical use cases — reminders, habit trackers, events in offline mode.
iOS: UNUserNotificationCenter
All APIs since iOS 10+ go through UNUserNotificationCenter. Three trigger types:
import UserNotifications
// 1. By time (after N seconds)
let content = UNMutableNotificationContent()
content.title = "Meeting reminder"
content.body = "Meeting with team in 15 minutes"
content.sound = .default
content.badge = 1
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 900, repeats: false)
let request = UNNotificationRequest(identifier: "meeting-reminder-123",
content: content,
trigger: trigger)
UNUserNotificationCenter.current().add(request)
// 2. By date/time (repeating every day at 9:00)
var dateComponents = DateComponents()
dateComponents.hour = 9
dateComponents.minute = 0
let dailyTrigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
// 3. By geofence
let region = CLCircularRegion(center: CLLocationCoordinate2D(latitude: 50.45, longitude: 30.52),
radius: 200,
identifier: "office-zone")
region.notifyOnEntry = true
region.notifyOnExit = false
let geoTrigger = UNLocationNotificationTrigger(region: region, repeats: false)
Request identifier (identifier) — important. Use it to update or cancel notification:
// Update (overwrite existing)
UNUserNotificationCenter.current().add(updatedRequest) // same identifier
// Cancel
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ["meeting-reminder-123"])
// View scheduled
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
print("Pending: \(requests.count)")
}
iOS limit — 64 scheduled notifications per app. When exceeded, old ones are silently removed. For trackers with hundreds of reminders, you need a strategy: keep nearest N in queue, reschedule the rest when app opens.
Android: NotificationCompat + AlarmManager / WorkManager
Android has no unified "notification scheduler" — you need to save the time yourself, wake the app through AlarmManager and then show notification.
// Schedule through AlarmManager (exact time — requires SCHEDULE_EXACT_ALARM on Android 12+)
val alarmManager = context.getSystemService(AlarmManager::class.java)
val intent = Intent(context, NotificationReceiver::class.java).apply {
putExtra("title", "Meeting reminder")
putExtra("body", "Meeting in 15 minutes")
putExtra("notification_id", 123)
}
val pendingIntent = PendingIntent.getBroadcast(context, 123, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerAtMillis,
pendingIntent
)
NotificationReceiver — a BroadcastReceiver that builds and shows notification:
class NotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val notification = NotificationCompat.Builder(context, "reminders_channel")
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(intent.getStringExtra("title"))
.setContentText(intent.getStringExtra("body"))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.build()
NotificationManagerCompat.from(context)
.notify(intent.getIntExtra("notification_id", 0), notification)
}
}
Important: AlarmManager resets on device reboot. You need to intercept BOOT_COMPLETED broadcast and reschedule all active reminders from local database (Room).
For Android 12+ exact alarms require SCHEDULE_EXACT_ALARM permission. For recurring reminders without exact time — better WorkManager with PeriodicWorkRequest, it survives reboot automatically.
Geofence Notifications
On iOS geofence notifications work through UNLocationNotificationTrigger (see above). On Android — through GeofencingClient:
val geofence = Geofence.Builder()
.setRequestId("office-zone")
.setCircularRegion(50.45, 30.52, 200f)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.build()
val request = GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofence(geofence)
.build()
geofencingClient.addGeofences(request, geofencePendingIntent)
Geofence notifications require ACCESS_FINE_LOCATION permission, and on Android 10+ — ACCESS_BACKGROUND_LOCATION. The latter is a separate request, user must explicitly select "Allow always" in settings.
Timeframe
Implementing local notifications by time and schedule, geofence triggers, correct reboot and limit handling — 3–6 working days depending on platforms.







