First, I'm going to suggest that we simplify the example:
class MyServiceClass {
private let timerSequence = Timer
.publish(every: 1, on: .main, in: .default)
.autoconnect()
.values
func setUpStreamIteration() async {
for await time in timerSequence {
print(time)
}
}
}
// It obviously doesn’t make much sense to create a local var for a service
// and then let it immediately fall out of scope, but we’re doing this just
// to illustrate the problem…
func foo() {
let service = MyServiceClass()
Task { await service.setUpStreamIteration() }
}
Once setUpStreamIteration
starts running, the MyServiceClass
cannot be released until setUpStreamIteration
finishes. But in the absence of something to stop this asynchronous sequence, this method will never finish. Inserting a weak
capture list in Task {…}
will not save you once setUpStreamIteration
starts. (It actually introduces a race between the deallocation of the service and the starting of this method, which complicates our attempts to reproduce/diagnose this problem.)
One approach, if you are using SwiftUI, is to create the stream in a .task
view modifier, and it will automatically cancel it when the view is dismissed. (Note, for this to work, one must remain within structured concurrency and avoid Task { … }
unstructured concurrency.)
The other typical solution is to explicitly opt into unstructured concurrency, save the Task
when you start the sequence and add a method to stop the sequence. E.g.:
class MyServiceClass {
private var task: Task<Void, Never>?
private let timerSequence = Timer
.publish(every: 1, on: .main, in: .default)
.autoconnect()
.values
func setUpStreamIteration() {
task = Task {
for await time in timerSequence {
print(time)
}
}
}
func stopStreamIteration() {
task?.cancel()
}
}
And you just need to make sure that you call stopStreamIteration
before you release it (e.g., when the associated view disappears).
By the way, if you want to avoid introducing Combine, you can use AsyncTimerSequence
from the Swift Async Algorithms package.
for await time in AsyncTimerSequence(interval: .seconds(1), clock: .continuous) {
print(time)
}
You still have the same issue about needing to cancel this sequence (using one of the two approaches outlined above), but at least it avoids introducing Combine into the mix). You could also write your own AsyncStream
wrapping Timer
or a GCD timer, but there's no reason to reinvent the wheel.