Let’s go through your questions one at a time:
This is my understanding of how the references work in the code above:
- The variable
model
holds a strong reference to an instance of the Model
class.
Correct, the view controller keeps that strong reference until the view controller, itself, is deallocated.
URLSession
holds a strong reference to its data task, which holds a strong reference to the closure.
The URLSession
maintains this strong reference until the request finishes/fails, at which point the data task is released. You do not need to keep a reference to the data task, as URLSession
automatically hangs on to it for the duration of the request anyway. That having been said, we often would keep our own weak
reference to it, which we would use if/when we might want to cancel the request. (See below.)
- The closure escapes the function, and because it needs to update self, it holds a strong reference to the Model instance
Yes, as it stands, the closure maintains a strong reference to self
, and this closure will not be released until the data task finishes or fails.
As an aside, we generally would not do that. Often we would use a [weak self]
capture list in this closure, so that it does not keep this strong reference to self
. E.g., you might:
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
// see if `self` was released
guard let self else { return }
// see if request succeeded
guard let data else {
print(error ?? URLError(.badServerResponse))
return
}
// if we got here, we have our `Data`; we always avoid forced unwrapping operators when dealing with data from a remote server
self.foo = data
}
- However, the
Model
instance does not hold a strong reference to the data task and therefore there is no reference cycle.
Yes. Or more accurately, as implemented in the question, the URLSession
will keep a strong reference to self
, will release that strong reference when the request finishes or fails. But, again, if we use [weak self]
capture list as outlined above, it won’t keep a strong reference at all, and the Model
will be deallocated as soon as the view controller is deallocated.
Even better, unless we explicitly need the task to continue running even if the Model
is deallocated for some reason, we would cancel
the task
when Model
is deallocated:
class Model {
var foo: Data?
private weak var task: URLSessionTask?
deinit {
task?.cancel()
}
func makeRequest(url: URL) {
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self else { return }
guard let data else {
print(error ?? URLError(.badServerResponse))
return
}
self.foo = data
}
task.resume()
self.task = task
}
}
Note, we neither need nor want to keep a strong reference to the URLSessionTask
. (The URLSession
will manage the lifecycle of the URLSessionTask
.) But we keep our own weak
reference, which will automatically be set to nil
when the URLSessionTask
is done. That way, if the request is not yet done when Model
is deallocated, we can cancel the request. But if the request is already done, that task
reference will be set to nil
automatically for us, in which case the task?.cancel()
, becomes a “no op”.