so I'm reading the Modern Concurrency book from raywenderlich.com and I assume the book must be outdated or something, I'm trying to run the closure
insde the AsyncStream
but it doesn't seem to get there, I'm still pretty new to this Async/Await thing, but when adding some breakpoints I can see my code is not getting there. This is my code and a screenshot with some warnings showing. I am not really familiar with what the warnings mean, just trying to learn all this new stuff, I would truly appreciate some help and is there a way to fix it with Swift 6? Thanks in advance!
Reference to captured var 'countdown' in concurrently-executing code; this is an error in Swift 6
Mutation of captured var 'countdown' in concurrently-executing code; this is an error in Swift 6
func countdown(to message: String) async throws {
guard !message.isEmpty else { return }
var countdown = 3
let counter = AsyncStream<String> { continuation in
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
continuation.yield("\(countdown)...")
countdown -= 1
}
}
for await countDownMessage in counter {
try await say(countDownMessage)
}
}
CodePudding user response:
Timer.scheduleTimer
requires that it be scheduled on a run loop. In practical terms, that means we would want to schedule it on the main thread’s run loop. So, you either call scheduleTimer
from the main thread, or create a Timer
and manually add(_:forMode:)
it to RunLoop.main
. See the Scheduling Timers in Run Loops section of the Timer
documentation.
The easiest way would be to just isolate this function to the main actor. E.g.,
@MainActor
func countdown(to message: String) async throws { … }
There a few other issues here, too:
I would suggest defining the
countdown
variable within theAsyncStream
:@MainActor func countdown(to message: String) async throws { guard !message.isEmpty else { return } let counter = AsyncStream<String> { continuation in var countdown = 3 Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in continuation.yield("\(countdown)...") countdown -= 1 } } for await countDownMessage in counter { try await say(countDownMessage) } }
The
AsyncStream
is never finished. You might want to finish it when it hits zero:@MainActor func countdown(to message: String) async throws { guard !message.isEmpty else { return } let counter = AsyncStream<String> { continuation in var countdown = 3 Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in continuation.yield("\(countdown)...") // presumably you want this countdown timer to finish when it hits zero guard countdown > 0 else { timer.invalidate() continuation.finish() return } // otherwise, decrement and carry on countdown -= 1 } } for await countDownMessage in counter { try await say(countDownMessage) } }
There should be a
continuation.onTermination
closure to handle cancelation of the asynchronous sequence.@MainActor func countdown(to message: String) async throws { guard !message.isEmpty else { return } let counter = AsyncStream<String> { continuation in var countdown = 3 let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in continuation.yield("\(countdown)...") // presumably you want this countdown timer to finish when it hits zero guard countdown > 0 else { timer.invalidate() continuation.finish() return } // otherwise, decrement and carry on countdown -= 1 } continuation.onTermination = { _ in timer.invalidate() } } for await countDownMessage in counter { try await say(countDownMessage) } }
Going back to the original question (why this is not running), I personally would avoid the use of Timer
in conjunction with Swift concurrency at all. A GCD timer would be better, as it doesn’t require a RunLoop
. Even better, I would advise Task.sleep
. Needless to say, that is designed to work with Swift concurrency, and also is cancelable.
I personally would suggest something like:
func countdown(to message: String) async throws {
guard !message.isEmpty else { return }
let counter = AsyncStream<String> { continuation in
let task = Task {
for countdown in (0...3).reversed() {
try await Task.sleep(for: .seconds(1))
continuation.yield("\(countdown)...")
}
continuation.finish()
}
continuation.onTermination = { _ in
task.cancel()
}
}
for await countDownMessage in counter {
try await say(countDownMessage)
}
}