Found this issue while working with the new Swift concurrency tools.
Here's the setup:
class FailedDeinit {
init() {
print(#function, id)
task = Task {
await subscribe()
}
}
deinit {
print(#function, id)
}
func subscribe() async {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
private var task: Task<(), Swift.Error>?
let id = UUID()
}
var instance: FailedDeinit? = FailedDeinit()
instance = nil
Running this code in a Playground yields this:
init() F007863C-9187-4591-A4F4-BC6BC990A935
!!! The deinit method is never called!!!
Strangely, when I change the code to this:
class SuccessDeinit {
init() {
print(#function, id)
task = Task {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
}
deinit {
print(#function, id)
}
private var task: Task<(), Swift.Error>?
let id = UUID()
}
var instance: SuccessDeinit? = SuccessDeinit()
instance = nil
By moving the code from the method subscribe() directly in the Task, the result in the console changes to this:
init() 0C455201-89AE-4D7A-90F8-D6B2D93493B1
deinit 0C455201-89AE-4D7A-90F8-D6B2D93493B1
This may be a bug or not but there is definitely something that I do not understand. I would welcome any insight about that.
~!~!~!~!
This is crazy (or maybe I am?) but with a SwiftUI macOS project. I still DON'T get the same behaviour as you. Look at that code where I kept the same definition of the FailedDeinit and SuccessDeinit classes but used them within a SwiftUI view.
struct ContentView: View {
@State private var failed: FailedDeinit?
@State private var success: SuccessDeinit?
var body: some View {
VStack {
HStack {
Button("Add failed") { failed = .init() }
Button("Remove failed") { failed = nil }
}
HStack {
Button("Add Success") { success = .init() }
Button("Remove Success") { success = nil }
}
}
}
}
class FailedDeinit {
init() {
print(#function, id)
task = Task { [weak self] in
await self?.subscribe()
}
}
deinit {
print(#function, id)
}
func subscribe() async {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
private var task: Task<(), Swift.Error>?
let id = UUID()
}
This doesn't really have anything to do with async/await or AsyncStream. It's a perfectly normal retain cycle. You (the FailedDeinit instance) are retaining the task, but the task refers to
subscribewhich is a method of you, i.e.self, so the task is retaining you. So simply break the retain cycle just like you would break any other retain cycle. Just changeTo
Also, be sure to test in a real project, not a playground, as playgrounds are not indicative of anything in this regard. Here's the code I used: