Answers
Great question! Implementing a countdown timer in a Kotlin Multiplatform (KMP) project that works on iOS requires a bit of platform-specific handling because iOS does not have a direct equivalent to Android’s CountDownTimer
class.
Here’s a straightforward approach to implement a countdown timer on iOS within a KMP project:
General Idea
- In shared code (commonMain): Define an interface or expect class for the countdown timer.
- In iOS source set: Implement the countdown timer using iOS-native APIs, such as
NSTimer
orDispatchSourceTimer
. - In Android source set: Use Android’s
CountDownTimer
as usual. - This way, your shared code calls the timer interface and the platform-specific implementations handle the details.
Step-by-step Example
1. Define the Timer interface in commonMain
interface CountdownTimer {
fun start()
fun cancel()
}
expect class PlatformCountdownTimer(
totalTimeMillis: Long,
intervalMillis: Long,
onTick: (remainingMillis: Long) -> Unit,
onFinish: () -> Unit
) : CountdownTimer
2. Implement on Android (androidMain)
actual class PlatformCountdownTimer actual constructor(
private val totalTimeMillis: Long,
private val intervalMillis: Long,
private val onTick: (Long) -> Unit,
private val onFinish: () -> Unit
) : CountdownTimer {
private val timer = object : android.os.CountDownTimer(totalTimeMillis, intervalMillis) {
override fun onTick(millisUntilFinished: Long) {
onTick(millisUntilFinished)
}
override fun onFinish() {
onFinish()
}
}
override fun start() {
timer.start()
}
override fun cancel() {
timer.cancel()
}
}
3. Implement on iOS (iosMain)
import platform.Foundation.NSTimer
import platform.Foundation.NSRunLoop
import platform.Foundation.NSDefaultRunLoopMode
import kotlin.native.concurrent.AtomicReference
import kotlin.native.concurrent.freeze
actual class PlatformCountdownTimer actual constructor(
private val totalTimeMillis: Long,
private val intervalMillis: Long,
private val onTick: (Long) -> Unit,
private val onFinish: () -> Unit
) : CountdownTimer {
private var timer: NSTimer? = null
private var remainingTime = totalTimeMillis
override fun start() {
stopTimer() // In case a timer is already running
timer = NSTimer.scheduledTimerWithTimeInterval(
intervalMillis / 1000.0,
repeats = true
) { _ ->
remainingTime -= intervalMillis
if (remainingTime > 0) {
onTick(remainingTime)
} else {
onFinish()
stopTimer()
}
}
// Make sure the timer runs on the main run loop so it fires properly
NSRunLoop.mainRunLoop.addTimer(timer!!, NSDefaultRunLoopMode)
}
override fun cancel() {
stopTimer()
}
private fun stopTimer() {
timer?.invalidate()
timer = null
}
}
4. Using the timer from shared code
fun createCountdownTimer(): CountdownTimer {
return PlatformCountdownTimer(
totalTimeMillis = 10000, // 10 seconds
intervalMillis = 1000, // 1 second intervals
onTick = { remaining ->
println("Remaining time: $remaining ms")
},
onFinish = {
println("Timer finished!")
}
)
}
Notes:
NSTimer
fires on the main thread — if your UI updates rely on this, it should be fine.- If you want more precise timing or background-safe timers, you could use
DispatchSourceTimer
from Grand Central Dispatch. - Don’t forget to manage lifecycle and cancel the timer when no longer needed to avoid memory leaks or unexpected behavior.
- You can wrap the timer logic in your own KMP abstraction for easy reuse.
If you want, I can also help with a DispatchSourceTimer
-based implementation for better precision on iOS or provide a full sample KMP project structure. Just let me know!