Code example:
func someAsyncFunc() async {
loadingTask?.cancel()
return await withCheckedContinuation { [weak self] checkedContinuation in
guard let self else {
return
}
loadingTask = Task { [weak self] in
guard let self else {
return
}
//some code with self
checkedContinuation.resume()
}
}
}
I understand that one of [weak self]
calls is extra - this example just shows 2 possible code chunks where it can be placed.
According to one documentation article withCheckedContinuation
should always call checkedContinuation.resume()
, according to another documentation article you should use [weak self]
to avoid strong reference cycles. But how to use both of them simultaneously?
If I leave code AS-IS then checkedContinuation.resume()
may not be called and it leads to memory leaks. If I don't use [weak self]
it leads to retain cycles which lead to memory leaks too. I also can't just replace withCheckedContinuation
with withCheckedThrowingContinuation
because it raises code complexity and also means I should forget about withCheckedContinuation
(because any similar method may contain [weak self]
).
A few observations:
We generally use [weak self]
to avoid strong reference cycles. There is no persistent strong reference cycle here, so [weak self]
is not needed.
Sometimes we used [weak self]
pattern in those situations where there was not a persistent strong reference cycle, but just a temporary reference to self
while the asynchronous task is running.
But even in this case, using [weak self]
is not well advised. Nowadays, rather than worrying about the lifespan of self
, we shift our attention to the lifespan of the asynchronous task. Specifically, we make sure we just cancel the the asynchronous work as soon as its results are no longer needed. This renders [weak self]
pattern moot.
Back in the days of GCD, cancelation was clumsy. But Swift concurrency offers first-class cancelation support, so we should embrace that, canceling the task when the view is dismissed (e.g., in UIKit/AppKit in viewDidDisappear
, in SwiftUI in .onDisappear
).
All of that having been said, let’s say that you really wanted to do something like contemplated in your question. (It is unnecessary and a bad design, but let us discuss it just as a thought exercise.)
Consider your code snippet:
func someAsyncFunc() async {
loadingTask?.cancel()
return await withCheckedContinuation { [weak self] checkedContinuation in
guard let self else {
return
}
loadingTask = Task { [weak self] in
guard let self else {
return
}
//some code with self
checkedContinuation.resume()
}
}
}
There are two problems with this guard let self else { return }
pattern:
First, tasks generally start immediately, so you generally will pass the guard
statements before self
is possibly deinitialized, so these guard
statements will not accomplish anything.
Second, as you noted, in these guard
statements, you are returning without calling resume
, which is invalid. In withCheckedContinuation
, you must resume
once and only once. But you are returning without ever calling resume
. If this happens (which is exceedingly unlikely, given my first point), the checked continuation will report an error. You must resume
the continuation before you return
in these guard
statements (assuming you guard
at all).
Note, when adopting cancelation patterns, you would generally use withTaskCancellationHandler
, try Task.checkCancellation()
, test Task.isCancelled()
, etc., in order to respond to the cancelation. But we would need to see the “some code with self” to advise further (and would probably be best addressed in a separate question). If you do not implement cancelation support, your task.cancel()
will not accomplish anything.
But, again, do not just blindly insert [weak self]
pattern for fear of a potential strong reference cycle risk, as there is no strong reference cycle here and it is a bit of an antipattern in Swift concurrency. One should adopt cancelation patterns, instead.