A stopwatch with 0.01-second precision and a countdown timer is a task that seems simple only at first glance. We, a team of mobile developers with many years of experience, have solved it dozens of times.
The main problem is not the beautiful UI, but that the app goes into the background, the phone locks, and when returning you need to show the correct time. Our solutions have passed App Store and Google Play moderation without a single delay.
Developing such a module usually takes from 2 to 5 days, and the cost is calculated after an audit depending on the feature set — this is cheaper than hiring a separate developer for a month (savings of up to 40%).
How to ensure accurate counting in the background?
Timer in iOS (also known as ScheduledTimer) is not suitable for accurate counting — it fires in the run loop and may be delayed under load on the main thread.
The correct approach: save startDate = Date() at start, and on each tick calculate elapsed = Date().timeIntervalSince(startDate). We update the UI via CADisplayLink for smoothness (60/120 fps) or Timer with a 0.01–0.1 s interval for ordinary needs.
When entering the background via NotificationCenter, we catch UIApplication.didEnterBackgroundNotification and capture backgroundDate. On willEnterForegroundNotification, we calculate the delta and correct the state.
For a timer with a notification — UNUserNotificationCenter.scheduleLocalNotification at start; on return we cancel it via removePendingNotificationRequests.
On Android — System.currentTimeMillis() or SystemClock.elapsedRealtime() for start (the latter is preferable — it does not depend on system time changes). Handler.postDelayed() for UI updates.
When entering the background via onPause(), we save the start time in the ViewModel; on onResume() we recalculate. For background timer operation — a ForegroundService with a notification in the status bar.
In Flutter — the Stopwatch class from Dart:core as the basis for the stopwatch (accurate, does not drift). Timer.periodic for the UI. In the background — flutter_foreground_task or a platform channel.
Why is Timer not suitable for mobile applications?
| Platform | Timer drawback | Alternative |
|---|---|---|
| iOS | Delay under load on the main thread | CADisplayLink with delta calculation |
| Android | Handler.postDelayed is tied to the UI thread | SystemClock.elapsedRealtime + Coroutine Timer |
| Flutter | Timer.periodic can drift | Stopwatch from dart:core + Timer for UI |
Our approach provides accuracy up to 0.001 s, which is 10 times more accurate than a typical Timer. We do not rely on a periodic timer for time counting, but use absolute timestamps. This guarantees correct time even under heavy load on the device.
What challenges arise when implementing laps?
Lap feature for a stopwatch: we store an array [(lapNumber: Int, lapTime: TimeInterval, totalTime: TimeInterval)]. The key challenge is updating the UI when adding each lap without flickering. We use diffing (SwiftUI List with animation or DiffUtil for Android).
Auto-scroll to the last element is implemented via ScrollViewReader (SwiftUI) or LazyListState (Compose). For long sessions (hundreds of laps), we use list virtualization and pagination. The table below compares the performance of different approaches:
| Method | Insert speed (1000 laps) | Memory usage |
|---|---|---|
| Array + pagination | 0.5 sec | 50 KB |
| LazyColumn/List | 0.8 sec | 30 KB |
| Without virtualization | 3.2 sec | 200 KB |
States and UI
Stopwatch: stopped, running, paused. Timer: idle, running, paused, finished. Each state is a specific set of available buttons and display.
Time display: HH:MM:SS.cc — formed from elapsed by calculation using integer division, not via DateFormatter (extra allocations on every tick). In SwiftUI — Text with monospacedDigit() so digits do not “jump” when values change. In Compose — FontVariation.Settings or a monospace font family.
Local notifications when the timer finishes
iOS: UNMutableNotificationContent + UNTimeIntervalNotificationTrigger with timeInterval equal to the remaining time. We request permission via UNUserNotificationCenter.requestAuthorization.
If the app is in the foreground — UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:) to show a banner.
Android: AlarmManager.setExactAndAllowWhileIdle() for exact firing with Doze mode. A BroadcastReceiver receives the intent and launches a notification via NotificationManager. From API 31+ it requires the SCHEDULE_EXACT_ALARM permission with an explanation to the user.
What is included in the work
- Source code with comments and documentation
- Setup of push notifications (APNs/FCM) and deep linking
- Integration with App Store Connect / Google Play Console
- Support for TestFlight and Firebase App Distribution
- 3-month warranty for bug fixes
- Consultations on publishing and navigating App Review (Section 4.2, 5.1)
Contact us to evaluate your project. Order development — get a ready module with guaranteed accuracy and compatibility. Get a consultation on choosing a stack and optimizing background processing.
Timeline: basic timer + stopwatch with background support — from 2 days. With laps, session history, custom sounds, and a home screen widget — from 5 days.







