Реалізація таймера та секундомера в мобільній програмі
Секундомер з точністю до 0.01 секунди та таймер зворотного відліку — завдання не така проста, як здається. Головна проблема не в UI, а в тому, що приложення йде в background, телефон блокується, та при повторенні потрібно показати правильний час.
Точність та background-поведінка
Timer у iOS (він же ScheduledTimer) не підходить для точного відліку — він срабатує в run loop та може затриматися при навантаженні на main thread. Правильний підхід: зберігаємо startDate = Date() при старті, в кожному тику обчислюємо elapsed = Date().timeIntervalSince(startDate). UI обновляємо через CADisplayLink для плавності (60/120 fps) або Timer з 0.01–0.1 с інтервалом для звичайних потреб.
При уходу в background через NotificationCenter ловимо UIApplication.didEnterBackgroundNotification, фіксуємо backgroundDate. При willEnterForegroundNotification обчислюємо дельту та коригуємо стан. Для таймера з сповіщенням — UNUserNotificationCenter.scheduleLocalNotification при старті; при поверненні скасовуємо через removePendingNotificationRequests.
На Android — System.currentTimeMillis() або SystemClock.elapsedRealtime() для старту (друга переважна — не залежить від зміни системного часу). Handler.postDelayed() для UI-обновлень. При уходу в background через onPause() зберігаємо стартовий час у ViewModel, при onResume() пересчітуємо. Для фонової роботи таймера — ForegroundService зі сповіщенням у статус-бару.
У Flutter — клас Stopwatch з Dart:core як базис для секундомера (точний, не дрейфує). Timer.periodic для UI. При background — flutter_foreground_task або платформенний канал.
Стани та UI
Секундомер: stopped, running, paused. Таймер: idle, running, paused, finished. Кожен стан — конкретний набір доступних кнопок та відображення.
Відображення часу: HH:MM:SS.cc — формуємо з elapsed обчисленням через цілочисельне ділення, не через DateFormatter (лишні allocations на кожний тик). У SwiftUI — Text з monospacedDigit() щоб цифри не «стрибали» при зміні значень. У Compose — FontVariation.Settings або monospace font family.
Lap-функція для секундомера: зберігаємо масив [(lapNumber: Int, lapTime: TimeInterval, totalTime: TimeInterval)], відображаємо у List / LazyColumn. Автоскролл до останнього елемента при додаванні.
Локальні сповіщення по закінченню таймера
iOS: UNMutableNotificationContent + UNTimeIntervalNotificationTrigger з timeInterval рівним залишками часу. Запитуємо дозвіл через UNUserNotificationCenter.requestAuthorization. Якщо приложення на переднему плані — UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:) для показу banner.
Android: AlarmManager.setExactAndAllowWhileIdle() для точного срабатування з Doze mode. BroadcastReceiver приймає intent, запускає сповіщення через NotificationManager. З API 31+ потребує дозволу SCHEDULE_EXACT_ALARM з поясненням користувачу.
Термін: базовий таймер + секундомер з background-підтримкою — 2 дні. З кругами, історією сесій, кастомними звуками та віджетом на домашньому екрані — 3 дні.







