A Repeating timer in Swift 6

I'm using that repeating timer for processing information repeatedly:

actor RepeatingTimer {
    private var task: Task<Void, Never>?
    private var isPaused = false
   
    func start(duration: Double, onTick: @escaping () -> Void) {
        task?.cancel() // Cancel any existing timer
        isPaused = false
        task = Task {
            while !Task.isCancelled {
                // Check if paused
                if !isPaused {
                    onTick()
                }
                // Sleep for the interval
                try? await Task.sleep(for: .seconds(duration))
            }
        }
    }

    func pause() {
        isPaused = true
    }

    func resume() {
        isPaused = false
    }

    func stop() {
        task?.cancel()
        task = nil
    }
}`

Yet when I call it from another actor with:

await timer.start(duration: interval, onTick:{
                self.process()
            })

I get:

Sending 'self'-isolated value of non-Sendable type '() -> ()' to actor-isolated instance method 'start(duration:onTick:)' risks causing races in between 'self'-isolated and actor-isolated uses

Is there some more stable option for managing repeating timers, or how to solve this error?

Answered by DTS Engineer in 886873022

Regarding your overall goal, Swift concurrency really needs a good equivalent to Foundation’s Timer, but I’ve yet to see any progress on that front. That’d be a good thing to raise via Swift Evolution.

Regarding your current approach, I suspect you can do a lot better than what you posted. If you want to explore improving this, my advice is that you start a thread on the Swift Forums, and specifically in Swift Forums > Using Swift area. I’ll definitely see that go by, and I’ll reply there with my own thoughts, but I’ll also be interested in hearing what the Swift experts have to say about this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

I fixed it by inserting @Sendable in the closure and changing the calling function to:

await timer.start(duration: interval, onTick:{
                Task.init{
                    await self.process()
                }
            })

Regarding your overall goal, Swift concurrency really needs a good equivalent to Foundation’s Timer, but I’ve yet to see any progress on that front. That’d be a good thing to raise via Swift Evolution.

Regarding your current approach, I suspect you can do a lot better than what you posted. If you want to explore improving this, my advice is that you start a thread on the Swift Forums, and specifically in Swift Forums > Using Swift area. I’ll definitely see that go by, and I’ll reply there with my own thoughts, but I’ll also be interested in hearing what the Swift experts have to say about this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Still my fix at least compiles, I shall see, when the full app compiles, if it also works.

So, if I were doing this I’d model the timer as an async sequence, which emits events each time the timer ‘fires’. And, in fact, the AsyncAlgorithms package already has this. Here’s a trivial example of how to use it:

import AsyncAlgorithms

func main() async {
    for await e in AsyncTimerSequence(interval: .seconds(1), clock: .continuous) {
        print(e)
    }
}

await main()

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

A Repeating timer in Swift 6
 
 
Q