Ah... about that. The article continually warns of how the other approaches can lead to retain cycles, but the problem here is that the recommended code is hiding one in plain sight.
Task { await handleViewModelEvents(for: viewModel) }
The Task used to kick things off is doing an implicit strong retain on self in order to call handleViewModelEvents.
And handleViewModelEvents will never return unless the channel is told to complete. Which it doesn't do.
So Channel never deallocates and leaks.
And that's the real problem with the recommended approach. When using non-completing asynchronous streams it's extremely easy to run into a retain cycle on the enclosing Task.
The easiest fix here is to change start.
func start() async {
await handleViewModelEvents(for: viewModel)
}
And call the above from a SwiftUI task modifier that's guaranteed to cancel. But even then you're going to need extra code to ensure .task doesn't trigger start more that once.