Секундомір з точністю до 0,01 секунди та таймер зворотного відліку — завдання, яке здається простим лише на перший погляд. Ми, команда мобільних розробників з багаторічним досвідом, вирішували її десятки разів. Головна проблема — не в гарному UI, а в тому, що додаток іде в background, телефон блокується, і при поверненні потрібно показати правильний час. Наші рішення пройшли модерацію App Store та Google Play без жодної затримки. Розробка такого модуля зазвичай займає від 2 до 5 днів і коштує від 2000 до 5000 залежно від набору функцій — це дешевше, ніж наймати окремого розробника на місяць (економія до 40%).
Як забезпечити точність відліку в фоні?
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 або платформний канал.
Чому Timer не підходить для мобільних додатків?
| Платформа | Недолік Timer | Альтернатива |
|---|---|---|
| iOS | Затримка при навантаженні main thread | CADisplayLink з обчисленням дельти |
| Android | Handler.postDelayed прив'язаний до UI-потоку | SystemClock.elapsedRealtime + Coroutine Timer |
| Flutter | Timer.periodic може дрейфувати | Stopwatch з dart:core + Timer для UI |
Наш підхід дає точність до 0,001 с, що в 10 разів точніше типового Timer. Ми не покладаємося на періодичний таймер для відліку часу, а використовуємо абсолютні мітки. Це гарантує коректний час навіть при сильному навантаженні на пристрій.
Які складнощі виникають при реалізації кіл (laps)?
Lap-функція для секундоміра: зберігаємо масив [(lapNumber: Int, lapTime: TimeInterval, totalTime: TimeInterval)]. Ключова складність — оновлення UI при додаванні кожного кола без мерехтіння. Використовуємо diffing (SwiftUI List з анімацією або DiffUtil для Android). Автоскрол до останнього елемента реалізується через ScrollViewReader (SwiftUI) або LazyListState (Compose). Для довгих сесій (сотні кіл) застосовуємо віртуалізацію списку та пагінацію. У таблиці нижче — порівняння продуктивності різних підходів:
| Метод | Швидкість вставки (1000 laps) | Витрати пам'яті |
|---|---|---|
| Масив + пагінація | 0,5 сек | 50 КБ |
| LazyColumn/List | 0,8 сек | 30 КБ |
| Без віртуалізації | 3,2 сек | 200 КБ |
Стани та UI
Секундомір: stopped, running, paused. Таймер: idle, running, paused, finished. Кожен стан — конкретний набір доступних кнопок та відображення.
Відображення часу: HH:MM:SS.cc — формуємо з elapsed обчисленням через цілочисельне ділення, не через DateFormatter (зайві алокації на кожен тік). У SwiftUI — Text з monospacedDigit(), щоб цифри не «стрибали» при зміні значень. У Compose — FontVariation.Settings або monospace font family.
Локальні сповіщення по закінченні таймера
iOS: UNMutableNotificationContent + UNTimeIntervalNotificationTrigger з timeInterval, що дорівнює часу, який залишився. Запитуємо дозвіл через UNUserNotificationCenter.requestAuthorization. Якщо додаток на передньому плані — UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:) для показу banner'а.
Android: AlarmManager.setExactAndAllowWhileIdle() для точного спрацьовування з Doze mode. BroadcastReceiver приймає intent, запускає сповіщення через NotificationManager. З API 31+ вимагає SCHEDULE_EXACT_ALARM дозволу з поясненням користувачеві.
Що входить в роботу
- Вихідний код з коментарями та документацією
- Налаштування push-сповіщень (APNs/FCM) та deep linking
- Інтеграція з App Store Connect / Google Play Console
- Підтримка TestFlight та Firebase App Distribution
- Гарантія 3 місяці на виправлення багів
- Консультації щодо публікації та обходу App Review (Section 4.2, 5.1)
Зв'яжіться з нами для безкоштовної оцінки вашого проекту. Замовте розробку — отримайте готовий модуль з гарантією точності та сумісності. Отримайте консультацію щодо вибору стека та оптимізації background-обробки.
Термін: базовий таймер + секундомір з background-підтримкою — від 2 днів. З колами, історією сесій, кастомними звуками та віджетом на домашньому екрані — від 5 днів.







